@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.
@@ -487,53 +487,104 @@ export class GraphSearch {
487
487
  }
488
488
  return;
489
489
  }
490
- // LIMIT 2 decides all three facts this rule needs — emptiness, plurality
491
- // (whether to consult the disambiguator), and the first-inserted
492
- // fallback without materialising a hub context's corpus-sized fan-out.
493
- const nx = this.store.nextFirst(it.node, 2);
494
- if (nx.length) {
495
- // A direct edge step along the chain toward its fixpoint. A recomposed
496
- // form (parts already rewritten and fused into a learned whole) follows
497
- // its continuation at MICRO, so reaching the grounded answer of the
498
- // recomposition beats leaving the parts split; the flag rides the chain so
499
- // every step of the recomposition's completion stays cheap.
500
- //
501
- // WHICH edge: a context node often carries SEVERAL learnt continuations
502
- // (the same sentence trained against 100+ target languages, a question
503
- // answered differently across sessions). `nx[0]` is an accident of
504
- // training order; when the host provides a `chooseNext` disambiguator
505
- // (see Mind.chooseNext) it picks the continuation with the most
506
- // distributional evidence (prevOf count the structural manifestation
507
- // of its halo), falling back to first-inserted when evidence is equal.
508
- yield {
509
- premises: [it],
510
- conclusion: {
511
- kind: "form",
512
- i: it.i,
513
- j: it.j,
514
- node: (nx.length > 1 ? this.host.chooseNext?.(it.node) : undefined) ??
515
- nx[0],
516
- via: true,
517
- rcmp: it.rcmp,
518
- },
519
- // A recomposed form's continuation is FREE: the two (or more) parts were
520
- // already paid for as their own rewrites, and the single consolidated
521
- // span saves one cover-bridge versus leaving them split so charging 0
522
- // here makes the grounded whole (e.g. "DE"→F) strictly beat the split
523
- // ("D","E") by exactly that saved bridge, deterministically.
524
- cost: it.rcmp ? 0 : STEP,
525
- };
526
- }
527
- else if (it.via) {
528
- // The chain reached a node with no WHOLE-node continuation. Before
529
- // emitting it as terminal, CONTINUE THE EXPLORATION into its own structure:
530
- // a composite answer like "p1 p2" leads nowhere as a whole, yet recognising
531
- // it surfaces p1, p2 — each of which continues ( R1, R2) and recomposes
532
- // into a deeper learnt form (→ FINAL). {@link recompleteNode} re-covers the
533
- // node's bytes through the SAME recognition + edge/fuse machinery the top
534
- // query uses (a continued graph exploration, not a re-perception), and
535
- // returns the deeper completion's bytes when it genuinely leads somewhere
536
- // new. Emit that; else emit the node itself as the terminal answer.
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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -707,52 +707,104 @@ export class GraphSearch {
707
707
  return;
708
708
  }
709
709
 
710
- // LIMIT 2 decides all three facts this rule needs — emptiness, plurality
711
- // (whether to consult the disambiguator), and the first-inserted
712
- // fallback without materialising a hub context's corpus-sized fan-out.
713
- const nx = this.store.nextFirst(it.node, 2);
714
- if (nx.length) {
715
- // A direct edge step along the chain toward its fixpoint. A recomposed
716
- // form (parts already rewritten and fused into a learned whole) follows
717
- // its continuation at MICRO, so reaching the grounded answer of the
718
- // recomposition beats leaving the parts split; the flag rides the chain so
719
- // every step of the recomposition's completion stays cheap.
720
- //
721
- // WHICH edge: a context node often carries SEVERAL learnt continuations
722
- // (the same sentence trained against 100+ target languages, a question
723
- // answered differently across sessions). `nx[0]` is an accident of
724
- // training order; when the host provides a `chooseNext` disambiguator
725
- // (see Mind.chooseNext) it picks the continuation with the most
726
- // distributional evidence (prevOf count — the structural manifestation
727
- // of its halo), falling back to first-inserted when evidence is equal.
728
- yield {
729
- premises: [it],
730
- conclusion: {
731
- kind: "form",
732
- i: it.i,
733
- j: it.j,
734
- node: (nx.length > 1 ? this.host.chooseNext?.(it.node) : undefined) ??
735
- nx[0],
736
- via: true,
737
- rcmp: it.rcmp,
738
- },
739
- // A recomposed form's continuation is FREE: the two (or more) parts were
740
- // already paid for as their own rewrites, and the single consolidated
741
- // span saves one cover-bridge versus leaving them split — so charging 0
742
- // here makes the grounded whole (e.g. "DE"→F) strictly beat the split
743
- // ("D","E") by exactly that saved bridge, deterministically.
744
- cost: it.rcmp ? 0 : STEP,
745
- };
746
- } else if (it.via) {
747
- // The chain reached a node with no WHOLE-node continuation. Before
748
- // emitting it as terminal, CONTINUE THE EXPLORATION into its own structure:
749
- // a composite answer like "p1 p2" leads nowhere as a whole, yet recognising
750
- // it surfaces p1, p2 each of which continues (→ R1, R2) and recomposes
751
- // into a deeper learnt form (→ FINAL). {@link recompleteNode} re-covers the
752
- // node's bytes through the SAME recognition + edge/fuse machinery the top
753
- // query uses (a continued graph exploration, not a re-perception), and
754
- // returns the deeper completion's bytes when it genuinely leads somewhere
755
- // new. Emit that; else emit the node itself as the terminal answer.
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
  ]);