@hviana/sema 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/mind/graph-search.js +127 -47
- package/dist/src/mind/mind.js +36 -0
- package/package.json +1 -1
- package/src/mind/graph-search.ts +128 -46
- package/src/mind/mind.ts +37 -0
|
@@ -487,53 +487,104 @@ export class GraphSearch {
|
|
|
487
487
|
}
|
|
488
488
|
return;
|
|
489
489
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
490
|
+
if (it.via) {
|
|
491
|
+
// A CHAIN hop (this form was itself reached by following an edge):
|
|
492
|
+
// read up to the hub bound — the SAME √(max(2,N)) convention {@link
|
|
493
|
+
// hubBound} in traverse.ts derives, replicated here because
|
|
494
|
+
// GraphSearch holds only a bare Store, not a MindContext — and fork
|
|
495
|
+
// across EVERY continuation. A bidirectional pair (SmolSent trains
|
|
496
|
+
// both directions) puts the back-edge to where we came from ahead of
|
|
497
|
+
// the forward edge in insertion order; a single first-inserted pick
|
|
498
|
+
// would walk the search straight into a cycle (the A*LD duplicate-key
|
|
499
|
+
// guard then dead-ends it) with no way to reach the forward edge.
|
|
500
|
+
// Forking offers every continuation as its own rule so the one that
|
|
501
|
+
// genuinely advances (not a duplicate) is still reachable.
|
|
502
|
+
const bound = Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
|
|
503
|
+
const nx = this.store.nextFirst(it.node, bound);
|
|
504
|
+
if (nx.length) {
|
|
505
|
+
// The SAME evidence-weighted disambiguation the first hop uses
|
|
506
|
+
// (below) identifies the most-corroborated continuation. Yielding
|
|
507
|
+
// it FIRST matters: {@link lightestDerivation}'s chart keeps the
|
|
508
|
+
// FIRST rule to reach a given cost for an item and discards later
|
|
509
|
+
// arrivals at an EQUAL cost (`cost < current`, strictly) — so
|
|
510
|
+
// among same-depth sibling forks that tie in cost, the
|
|
511
|
+
// evidence-backed edge wins deterministically, never by
|
|
512
|
+
// exploration-order luck. `preferred`, when set, is necessarily
|
|
513
|
+
// an element of `nx` (chooseNext reads the identical hub-bounded
|
|
514
|
+
// set — see traverse.ts), so a plain skip-in-place suffices; no
|
|
515
|
+
// second array need be allocated to reorder it to the front.
|
|
516
|
+
const preferred = nx.length > 1
|
|
517
|
+
? this.host.chooseNext?.(it.node)
|
|
518
|
+
: undefined;
|
|
519
|
+
const fork = (node) => ({
|
|
520
|
+
premises: [it],
|
|
521
|
+
conclusion: { kind: "form", i: it.i, j: it.j, node, via: true },
|
|
522
|
+
// A real edge always costs STEP, whether it is the first hop or
|
|
523
|
+
// the fifth — so a chain's total cost is proportional to its
|
|
524
|
+
// length and the lightest derivation is always the SHORTEST
|
|
525
|
+
// successful one. (Charging every hop after the first for
|
|
526
|
+
// FREE, as an earlier design did, made every stopping point at
|
|
527
|
+
// any depth tie at the same cost — indistinguishable to the
|
|
528
|
+
// search, decided by whichever arrived first.)
|
|
529
|
+
cost: STEP,
|
|
530
|
+
});
|
|
531
|
+
if (preferred !== undefined)
|
|
532
|
+
yield fork(preferred);
|
|
533
|
+
for (const c of nx) {
|
|
534
|
+
if (c !== preferred)
|
|
535
|
+
yield fork(c);
|
|
536
|
+
}
|
|
537
|
+
// Also offer stopping HERE, at CONCEPT above whatever the chain
|
|
538
|
+
// has already cost — the same "a synonym jump is dearer than a
|
|
539
|
+
// direct edge" ordering the ladder already uses elsewhere,
|
|
540
|
+
// repurposed as "giving up early is dearer than one more real
|
|
541
|
+
// hop". A premature stop at depth D costs D·STEP + CONCEPT, so a
|
|
542
|
+
// SHORTER premature stop always beats a longer one, and a genuine
|
|
543
|
+
// fixpoint (below, costing +0) always beats any premature stop at
|
|
544
|
+
// the same depth — the search only settles here when continuing
|
|
545
|
+
// genuinely dead-ends (every fork above revisits an already-
|
|
546
|
+
// explored key) or grows costlier than giving up.
|
|
547
|
+
//
|
|
548
|
+
// The stop-here bytes are the node's OWN, as-is — NOT recompleteNode's
|
|
549
|
+
// recursive re-cover. recompleteNode re-runs the full recognition
|
|
550
|
+
// + edge/fuse search over the node's bytes, an O(node's own
|
|
551
|
+
// substructure) cost independent of chain depth; offering it at
|
|
552
|
+
// EVERY premature stop makes a query's total cost scale with how
|
|
553
|
+
// many nodes the corpus happens to interconnect here (corpus
|
|
554
|
+
// density), not with the answer's own hop count — the exact
|
|
555
|
+
// output-sensitivity the rest of this search is built to keep.
|
|
556
|
+
// Reserved below for the ONE place it is load-bearing: the true
|
|
557
|
+
// fixpoint, checked once, at the chain's actual end.
|
|
558
|
+
yield {
|
|
559
|
+
premises: [it],
|
|
560
|
+
conclusion: {
|
|
561
|
+
kind: "out",
|
|
562
|
+
i: it.i,
|
|
563
|
+
j: it.j,
|
|
564
|
+
bytes: nodeBytes(it.node),
|
|
565
|
+
cover: true,
|
|
566
|
+
rec: true,
|
|
567
|
+
node: it.node,
|
|
568
|
+
},
|
|
569
|
+
cost: CONCEPT,
|
|
570
|
+
};
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
// The chain reached a node with no WHOLE-node continuation anywhere
|
|
574
|
+
// — a genuine fixpoint, not a premature stop. Before emitting it as
|
|
575
|
+
// terminal, CONTINUE THE EXPLORATION into its own structure: a
|
|
576
|
+
// composite answer like "p1 p2" leads nowhere as a whole, yet
|
|
577
|
+
// recognising it surfaces p1, p2 — each of which continues (→ R1,
|
|
578
|
+
// R2) and recomposes into a deeper learnt form (→ FINAL). {@link
|
|
579
|
+
// recompleteNode} re-covers the node's bytes through the SAME
|
|
580
|
+
// recognition + edge/fuse machinery the top query uses (a continued
|
|
581
|
+
// graph exploration, not a re-perception), and returns the deeper
|
|
582
|
+
// completion's bytes when it genuinely leads somewhere new. Emit
|
|
583
|
+
// that; else emit the node itself as the terminal answer. This is
|
|
584
|
+
// the ONE place the recursive re-cover runs — once, at the chain's
|
|
585
|
+
// actual end, never per intermediate stop — so its cost tracks the
|
|
586
|
+
// ANSWER's own structure, not how densely the corpus interconnects
|
|
587
|
+
// the nodes passed through on the way there.
|
|
537
588
|
const deeper = this.recompleteNode(it.node);
|
|
538
589
|
yield {
|
|
539
590
|
premises: [it],
|
|
@@ -550,6 +601,35 @@ export class GraphSearch {
|
|
|
550
601
|
};
|
|
551
602
|
}
|
|
552
603
|
else {
|
|
604
|
+
// The FIRST hop out of a recognised site — unchanged from the
|
|
605
|
+
// original design. LIMIT 2 only senses PLURALITY (does this node
|
|
606
|
+
// have more than one learnt continuation?); the actual bounded read
|
|
607
|
+
// and evidence-weighted pick both live inside {@link
|
|
608
|
+
// Mind.chooseNext} (see traverse.ts), which floors its own hub bound
|
|
609
|
+
// at √(max(2,N)) regardless of what we read here.
|
|
610
|
+
const nx = this.store.nextFirst(it.node, 2);
|
|
611
|
+
if (nx.length) {
|
|
612
|
+
yield {
|
|
613
|
+
premises: [it],
|
|
614
|
+
conclusion: {
|
|
615
|
+
kind: "form",
|
|
616
|
+
i: it.i,
|
|
617
|
+
j: it.j,
|
|
618
|
+
node: (nx.length > 1 ? this.host.chooseNext?.(it.node) : undefined) ??
|
|
619
|
+
nx[0],
|
|
620
|
+
via: true,
|
|
621
|
+
rcmp: it.rcmp,
|
|
622
|
+
},
|
|
623
|
+
// A recomposed form's continuation is FREE: the two (or more) parts
|
|
624
|
+
// were already paid for as their own rewrites, and the single
|
|
625
|
+
// consolidated span saves one cover-bridge versus leaving them
|
|
626
|
+
// split — so charging 0 here makes the grounded whole (e.g.
|
|
627
|
+
// "DE"→F) strictly beat the split ("D","E") by exactly that saved
|
|
628
|
+
// bridge, deterministically.
|
|
629
|
+
cost: it.rcmp ? 0 : STEP,
|
|
630
|
+
};
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
553
633
|
// Recognised but edge-less: borrow a concept (halo) sibling's edge. No
|
|
554
634
|
// edge and no concept means the form leads nowhere — it yields no rule, so
|
|
555
635
|
// a query of only such forms produces no derivation, and think is silent.
|
package/dist/src/mind/mind.js
CHANGED
|
@@ -377,6 +377,42 @@ export class Mind {
|
|
|
377
377
|
this.canon = this._canonFor(typeof turn === "string" ? textCanon : null);
|
|
378
378
|
this.canonMemo = this.canon ? new Map() : null;
|
|
379
379
|
try {
|
|
380
|
+
// Seed the recognise memo with the STANDALONE recognition of the
|
|
381
|
+
// NEW turn, offset into the accumulated context. In the full-context
|
|
382
|
+
// tree, content-defined chunk boundaries shift when bytes are
|
|
383
|
+
// concatenated — foldTree no longer visits the original turn's root
|
|
384
|
+
// node, so the structural pass misses it. Perceiving the turn alone
|
|
385
|
+
// preserves intact structure. We seed ONLY the suffix (the new turn);
|
|
386
|
+
// prefix forms stay visible through the full-context recognition
|
|
387
|
+
// (they serve as context, not as questions to re-answer).
|
|
388
|
+
if (data.boundaries.length > 0) {
|
|
389
|
+
const suffixStart = data.boundaries[data.boundaries.length - 1];
|
|
390
|
+
if (suffixStart < newContext.length) {
|
|
391
|
+
const suffixBytes = newContext.subarray(suffixStart);
|
|
392
|
+
const suffixRec = recognise(this, suffixBytes);
|
|
393
|
+
const fullRec = recognise(this, newContext);
|
|
394
|
+
const seen = new Set(fullRec.sites.map((s) => `${s.start},${s.end}`));
|
|
395
|
+
const mergedSites = [...fullRec.sites];
|
|
396
|
+
for (const site of suffixRec.sites) {
|
|
397
|
+
const absStart = suffixStart + site.start;
|
|
398
|
+
const absEnd = suffixStart + site.end;
|
|
399
|
+
const key = `${absStart},${absEnd}`;
|
|
400
|
+
if (!seen.has(key)) {
|
|
401
|
+
seen.add(key);
|
|
402
|
+
mergedSites.push({
|
|
403
|
+
start: absStart,
|
|
404
|
+
end: absEnd,
|
|
405
|
+
payload: site.payload,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
data.recogniseMemo.set(latin1Key(newContext), {
|
|
410
|
+
sites: mergedSites,
|
|
411
|
+
leaves: fullRec.leaves,
|
|
412
|
+
splits: fullRec.splits,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
380
416
|
const top = this.trace?.enter("respondTurn", [
|
|
381
417
|
rItem(newContext, "query"),
|
|
382
418
|
]);
|
package/package.json
CHANGED
package/src/mind/graph-search.ts
CHANGED
|
@@ -707,52 +707,104 @@ export class GraphSearch {
|
|
|
707
707
|
return;
|
|
708
708
|
}
|
|
709
709
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
//
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
710
|
+
if (it.via) {
|
|
711
|
+
// A CHAIN hop (this form was itself reached by following an edge):
|
|
712
|
+
// read up to the hub bound — the SAME √(max(2,N)) convention {@link
|
|
713
|
+
// hubBound} in traverse.ts derives, replicated here because
|
|
714
|
+
// GraphSearch holds only a bare Store, not a MindContext — and fork
|
|
715
|
+
// across EVERY continuation. A bidirectional pair (SmolSent trains
|
|
716
|
+
// both directions) puts the back-edge to where we came from ahead of
|
|
717
|
+
// the forward edge in insertion order; a single first-inserted pick
|
|
718
|
+
// would walk the search straight into a cycle (the A*LD duplicate-key
|
|
719
|
+
// guard then dead-ends it) with no way to reach the forward edge.
|
|
720
|
+
// Forking offers every continuation as its own rule so the one that
|
|
721
|
+
// genuinely advances (not a duplicate) is still reachable.
|
|
722
|
+
const bound = Math.ceil(
|
|
723
|
+
Math.sqrt(Math.max(2, this.store.edgeSourceCount())),
|
|
724
|
+
);
|
|
725
|
+
const nx = this.store.nextFirst(it.node, bound);
|
|
726
|
+
if (nx.length) {
|
|
727
|
+
// The SAME evidence-weighted disambiguation the first hop uses
|
|
728
|
+
// (below) identifies the most-corroborated continuation. Yielding
|
|
729
|
+
// it FIRST matters: {@link lightestDerivation}'s chart keeps the
|
|
730
|
+
// FIRST rule to reach a given cost for an item and discards later
|
|
731
|
+
// arrivals at an EQUAL cost (`cost < current`, strictly) — so
|
|
732
|
+
// among same-depth sibling forks that tie in cost, the
|
|
733
|
+
// evidence-backed edge wins deterministically, never by
|
|
734
|
+
// exploration-order luck. `preferred`, when set, is necessarily
|
|
735
|
+
// an element of `nx` (chooseNext reads the identical hub-bounded
|
|
736
|
+
// set — see traverse.ts), so a plain skip-in-place suffices; no
|
|
737
|
+
// second array need be allocated to reorder it to the front.
|
|
738
|
+
const preferred = nx.length > 1
|
|
739
|
+
? this.host.chooseNext?.(it.node)
|
|
740
|
+
: undefined;
|
|
741
|
+
const fork = (node: number): Rule<GItem> => ({
|
|
742
|
+
premises: [it],
|
|
743
|
+
conclusion: { kind: "form", i: it.i, j: it.j, node, via: true },
|
|
744
|
+
// A real edge always costs STEP, whether it is the first hop or
|
|
745
|
+
// the fifth — so a chain's total cost is proportional to its
|
|
746
|
+
// length and the lightest derivation is always the SHORTEST
|
|
747
|
+
// successful one. (Charging every hop after the first for
|
|
748
|
+
// FREE, as an earlier design did, made every stopping point at
|
|
749
|
+
// any depth tie at the same cost — indistinguishable to the
|
|
750
|
+
// search, decided by whichever arrived first.)
|
|
751
|
+
cost: STEP,
|
|
752
|
+
});
|
|
753
|
+
if (preferred !== undefined) yield fork(preferred);
|
|
754
|
+
for (const c of nx) {
|
|
755
|
+
if (c !== preferred) yield fork(c);
|
|
756
|
+
}
|
|
757
|
+
// Also offer stopping HERE, at CONCEPT above whatever the chain
|
|
758
|
+
// has already cost — the same "a synonym jump is dearer than a
|
|
759
|
+
// direct edge" ordering the ladder already uses elsewhere,
|
|
760
|
+
// repurposed as "giving up early is dearer than one more real
|
|
761
|
+
// hop". A premature stop at depth D costs D·STEP + CONCEPT, so a
|
|
762
|
+
// SHORTER premature stop always beats a longer one, and a genuine
|
|
763
|
+
// fixpoint (below, costing +0) always beats any premature stop at
|
|
764
|
+
// the same depth — the search only settles here when continuing
|
|
765
|
+
// genuinely dead-ends (every fork above revisits an already-
|
|
766
|
+
// explored key) or grows costlier than giving up.
|
|
767
|
+
//
|
|
768
|
+
// The stop-here bytes are the node's OWN, as-is — NOT recompleteNode's
|
|
769
|
+
// recursive re-cover. recompleteNode re-runs the full recognition
|
|
770
|
+
// + edge/fuse search over the node's bytes, an O(node's own
|
|
771
|
+
// substructure) cost independent of chain depth; offering it at
|
|
772
|
+
// EVERY premature stop makes a query's total cost scale with how
|
|
773
|
+
// many nodes the corpus happens to interconnect here (corpus
|
|
774
|
+
// density), not with the answer's own hop count — the exact
|
|
775
|
+
// output-sensitivity the rest of this search is built to keep.
|
|
776
|
+
// Reserved below for the ONE place it is load-bearing: the true
|
|
777
|
+
// fixpoint, checked once, at the chain's actual end.
|
|
778
|
+
yield {
|
|
779
|
+
premises: [it],
|
|
780
|
+
conclusion: {
|
|
781
|
+
kind: "out",
|
|
782
|
+
i: it.i,
|
|
783
|
+
j: it.j,
|
|
784
|
+
bytes: nodeBytes(it.node),
|
|
785
|
+
cover: true,
|
|
786
|
+
rec: true,
|
|
787
|
+
node: it.node,
|
|
788
|
+
},
|
|
789
|
+
cost: CONCEPT,
|
|
790
|
+
};
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
// The chain reached a node with no WHOLE-node continuation anywhere
|
|
794
|
+
// — a genuine fixpoint, not a premature stop. Before emitting it as
|
|
795
|
+
// terminal, CONTINUE THE EXPLORATION into its own structure: a
|
|
796
|
+
// composite answer like "p1 p2" leads nowhere as a whole, yet
|
|
797
|
+
// recognising it surfaces p1, p2 — each of which continues (→ R1,
|
|
798
|
+
// R2) and recomposes into a deeper learnt form (→ FINAL). {@link
|
|
799
|
+
// recompleteNode} re-covers the node's bytes through the SAME
|
|
800
|
+
// recognition + edge/fuse machinery the top query uses (a continued
|
|
801
|
+
// graph exploration, not a re-perception), and returns the deeper
|
|
802
|
+
// completion's bytes when it genuinely leads somewhere new. Emit
|
|
803
|
+
// that; else emit the node itself as the terminal answer. This is
|
|
804
|
+
// the ONE place the recursive re-cover runs — once, at the chain's
|
|
805
|
+
// actual end, never per intermediate stop — so its cost tracks the
|
|
806
|
+
// ANSWER's own structure, not how densely the corpus interconnects
|
|
807
|
+
// the nodes passed through on the way there.
|
|
756
808
|
const deeper = this.recompleteNode(it.node);
|
|
757
809
|
yield {
|
|
758
810
|
premises: [it],
|
|
@@ -768,6 +820,36 @@ export class GraphSearch {
|
|
|
768
820
|
cost: 0,
|
|
769
821
|
};
|
|
770
822
|
} else {
|
|
823
|
+
// The FIRST hop out of a recognised site — unchanged from the
|
|
824
|
+
// original design. LIMIT 2 only senses PLURALITY (does this node
|
|
825
|
+
// have more than one learnt continuation?); the actual bounded read
|
|
826
|
+
// and evidence-weighted pick both live inside {@link
|
|
827
|
+
// Mind.chooseNext} (see traverse.ts), which floors its own hub bound
|
|
828
|
+
// at √(max(2,N)) regardless of what we read here.
|
|
829
|
+
const nx = this.store.nextFirst(it.node, 2);
|
|
830
|
+
if (nx.length) {
|
|
831
|
+
yield {
|
|
832
|
+
premises: [it],
|
|
833
|
+
conclusion: {
|
|
834
|
+
kind: "form",
|
|
835
|
+
i: it.i,
|
|
836
|
+
j: it.j,
|
|
837
|
+
node:
|
|
838
|
+
(nx.length > 1 ? this.host.chooseNext?.(it.node) : undefined) ??
|
|
839
|
+
nx[0],
|
|
840
|
+
via: true,
|
|
841
|
+
rcmp: it.rcmp,
|
|
842
|
+
},
|
|
843
|
+
// A recomposed form's continuation is FREE: the two (or more) parts
|
|
844
|
+
// were already paid for as their own rewrites, and the single
|
|
845
|
+
// consolidated span saves one cover-bridge versus leaving them
|
|
846
|
+
// split — so charging 0 here makes the grounded whole (e.g.
|
|
847
|
+
// "DE"→F) strictly beat the split ("D","E") by exactly that saved
|
|
848
|
+
// bridge, deterministically.
|
|
849
|
+
cost: it.rcmp ? 0 : STEP,
|
|
850
|
+
};
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
771
853
|
// Recognised but edge-less: borrow a concept (halo) sibling's edge. No
|
|
772
854
|
// edge and no concept means the form leads nowhere — it yields no rule, so
|
|
773
855
|
// a query of only such forms produces no derivation, and think is silent.
|
package/src/mind/mind.ts
CHANGED
|
@@ -644,6 +644,43 @@ export class Mind implements MindContext {
|
|
|
644
644
|
this.canonMemo = this.canon ? new Map() : null;
|
|
645
645
|
|
|
646
646
|
try {
|
|
647
|
+
// Seed the recognise memo with the STANDALONE recognition of the
|
|
648
|
+
// NEW turn, offset into the accumulated context. In the full-context
|
|
649
|
+
// tree, content-defined chunk boundaries shift when bytes are
|
|
650
|
+
// concatenated — foldTree no longer visits the original turn's root
|
|
651
|
+
// node, so the structural pass misses it. Perceiving the turn alone
|
|
652
|
+
// preserves intact structure. We seed ONLY the suffix (the new turn);
|
|
653
|
+
// prefix forms stay visible through the full-context recognition
|
|
654
|
+
// (they serve as context, not as questions to re-answer).
|
|
655
|
+
if (data.boundaries.length > 0) {
|
|
656
|
+
const suffixStart = data.boundaries[data.boundaries.length - 1];
|
|
657
|
+
if (suffixStart < newContext.length) {
|
|
658
|
+
const suffixBytes = newContext.subarray(suffixStart);
|
|
659
|
+
const suffixRec = recognise(this, suffixBytes);
|
|
660
|
+
const fullRec = recognise(this, newContext);
|
|
661
|
+
const seen = new Set(fullRec.sites.map((s) => `${s.start},${s.end}`));
|
|
662
|
+
const mergedSites = [...fullRec.sites];
|
|
663
|
+
for (const site of suffixRec.sites) {
|
|
664
|
+
const absStart = suffixStart + site.start;
|
|
665
|
+
const absEnd = suffixStart + site.end;
|
|
666
|
+
const key = `${absStart},${absEnd}`;
|
|
667
|
+
if (!seen.has(key)) {
|
|
668
|
+
seen.add(key);
|
|
669
|
+
mergedSites.push({
|
|
670
|
+
start: absStart,
|
|
671
|
+
end: absEnd,
|
|
672
|
+
payload: site.payload,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
data.recogniseMemo.set(latin1Key(newContext), {
|
|
677
|
+
sites: mergedSites,
|
|
678
|
+
leaves: fullRec.leaves,
|
|
679
|
+
splits: fullRec.splits,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
647
684
|
const top = this.trace?.enter("respondTurn", [
|
|
648
685
|
rItem(newContext, "query"),
|
|
649
686
|
]);
|