@hviana/sema 0.4.3 → 0.4.4

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.
@@ -553,30 +553,54 @@ async function bridgeImpl(ctx, query, proposed) {
553
553
  // hundreds of roots), so this is where its proposals are first sized.
554
554
  //
555
555
  // Candidate bytes are read LAZILY — on first access during the seed
556
- // check — not eagerly for every collected id. On a 325K-context store
557
- // the climb channel alone can propose hundreds of edge-bearing ancestors
558
- // (hubBound = 571), most of which will never contain a picked anchor
559
- // window and would be discarded at the seed check without their bytes
560
- // ever being consulted. Eager reads for 500+ candidates each traversing
561
- // the DAG (profiled at 12K node records and 73KB of bytes read per
562
- // refusing query) is the dominant remaining bridge cost after the ANN
563
- // gate. A Map stays available for the frame-unanimity scan below, which
564
- // only needs bytes of candidates that actually seeded.
565
- const seededBytes = new Map();
566
- /** Read a candidate's bytes once; cache for the seed check AND for the
567
- * frame-unanimity scan that follows alignment. Returns null when the
568
- * candidate exceeds the phrase-scale cap or has no content. */
556
+ // check — not eagerly for every collected id. Most climb-proposed
557
+ // candidates fail the seed check and never reach the expensive identity
558
+ // and frame-consensus gates.
559
+ //
560
+ // Frame unanimity is different: once any candidate reaches that gate, it
561
+ // must be evaluated against the COMPLETE collected candidate population,
562
+ // not only the prefix whose bytes happened to be loaded earlier. The full
563
+ // phrase-scale population is therefore materialised once, lazily, on the
564
+ // first unanimous() call and reused afterward.
565
+ //
566
+ // Null results are memoised too, so an empty or over-cap candidate is never
567
+ // read repeatedly by the candidate loop and the population materialiser.
568
+ const candidateByteMemo = new Map();
569
+ /** Read one candidate at most once. Returns null when it exceeds the
570
+ * phrase-scale cap or has no content. */
569
571
  const bytesOfCandidate = (sid) => {
570
- const hit = seededBytes.get(sid);
571
- if (hit !== undefined)
572
- return hit;
572
+ const cached = candidateByteMemo.get(sid);
573
+ if (cached !== undefined)
574
+ return cached;
573
575
  const b = candidateBytes(sid);
574
- if (b !== null)
575
- seededBytes.set(sid, b);
576
+ candidateByteMemo.set(sid, b);
576
577
  return b;
577
578
  };
578
- if (diagnostics)
579
- diagnostics.phraseScale = seededBytes.size;
579
+ let framePopulation = null;
580
+ /** Return the complete phrase-scale candidate population.
581
+ *
582
+ * This is intentionally lazy: queries that never reach frame unanimity
583
+ * keep the cheap per-candidate seed path. Once required, every candidate
584
+ * is bounded by candidateBytes(), loaded at most once, and all subsequent
585
+ * unanimity checks observe the same order-independent population.
586
+ */
587
+ const ensureFramePopulation = () => {
588
+ if (framePopulation !== null) {
589
+ return framePopulation;
590
+ }
591
+ const complete = new Map();
592
+ for (const sid of candidates) {
593
+ const bytes = bytesOfCandidate(sid);
594
+ if (bytes !== null) {
595
+ complete.set(sid, bytes);
596
+ }
597
+ }
598
+ framePopulation = complete;
599
+ if (diagnostics) {
600
+ diagnostics.phraseScale = complete.size;
601
+ }
602
+ return complete;
603
+ };
580
604
  // FRAME UNANIMITY: a substitution U → C inside the frame (Lf, Rf) is
581
605
  // groundable only when the collected candidates — the store's own sample
582
606
  // of contexts sharing the query's content — are unanimous about the
@@ -595,7 +619,8 @@ async function bridgeImpl(ctx, query, proposed) {
595
619
  // the substitution was accepted). "Unanimous" must mean the store's own
596
620
  // instances agree, which requires at least one instance to consult.
597
621
  const unanimous = (u, c, lf, rf) => {
598
- for (const bytes of seededBytes.values()) {
622
+ const population = ensureFramePopulation();
623
+ for (const bytes of population.values()) {
599
624
  let from = 0;
600
625
  for (;;) {
601
626
  const i = indexOf(bytes, lf, from);
@@ -219,8 +219,17 @@ export class Mind {
219
219
  // Inference is a pure function of cumulative bytes. Conversation
220
220
  // boundaries remain persistence/API metadata and must not select a
221
221
  // different mechanism path than respond() on the identical byte stream.
222
- this.answeredSpans = [];
223
- this.currentTurnStart = 0;
222
+ // answeredSpans and currentTurnStart ARE restored from the conversation,
223
+ // however — they are pure functions of the cumulative byte stream (the
224
+ // assistant's own prior replies, and where the current user turn starts,
225
+ // are deterministic given the full transcript). Without them confluence,
226
+ // cover, the weave, and the consensus climb treat prior assistant turns
227
+ // as fresh query content — re-deriving them as constraints, voting
228
+ // anchors, and alignment points.
229
+ this.answeredSpans = conv ? conv.answeredSpans : [];
230
+ this.currentTurnStart = conv && conv.boundaries.length > 0
231
+ ? conv.boundaries[conv.boundaries.length - 1]
232
+ : 0;
224
233
  this.canon = canon ?? null;
225
234
  this.canonMemo = canon ? new Map() : null;
226
235
  this._beginMeter();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -594,27 +594,62 @@ async function bridgeImpl(
594
594
  // hundreds of roots), so this is where its proposals are first sized.
595
595
  //
596
596
  // Candidate bytes are read LAZILY — on first access during the seed
597
- // check — not eagerly for every collected id. On a 325K-context store
598
- // the climb channel alone can propose hundreds of edge-bearing ancestors
599
- // (hubBound = 571), most of which will never contain a picked anchor
600
- // window and would be discarded at the seed check without their bytes
601
- // ever being consulted. Eager reads for 500+ candidates each traversing
602
- // the DAG (profiled at 12K node records and 73KB of bytes read per
603
- // refusing query) is the dominant remaining bridge cost after the ANN
604
- // gate. A Map stays available for the frame-unanimity scan below, which
605
- // only needs bytes of candidates that actually seeded.
606
- const seededBytes = new Map<number, Uint8Array>();
607
- /** Read a candidate's bytes once; cache for the seed check AND for the
608
- * frame-unanimity scan that follows alignment. Returns null when the
609
- * candidate exceeds the phrase-scale cap or has no content. */
597
+ // check — not eagerly for every collected id. Most climb-proposed
598
+ // candidates fail the seed check and never reach the expensive identity
599
+ // and frame-consensus gates.
600
+ //
601
+ // Frame unanimity is different: once any candidate reaches that gate, it
602
+ // must be evaluated against the COMPLETE collected candidate population,
603
+ // not only the prefix whose bytes happened to be loaded earlier. The full
604
+ // phrase-scale population is therefore materialised once, lazily, on the
605
+ // first unanimous() call and reused afterward.
606
+ //
607
+ // Null results are memoised too, so an empty or over-cap candidate is never
608
+ // read repeatedly by the candidate loop and the population materialiser.
609
+ const candidateByteMemo = new Map<number, Uint8Array | null>();
610
+
611
+ /** Read one candidate at most once. Returns null when it exceeds the
612
+ * phrase-scale cap or has no content. */
610
613
  const bytesOfCandidate = (sid: number): Uint8Array | null => {
611
- const hit = seededBytes.get(sid);
612
- if (hit !== undefined) return hit;
614
+ const cached = candidateByteMemo.get(sid);
615
+ if (cached !== undefined) return cached;
616
+
613
617
  const b = candidateBytes(sid);
614
- if (b !== null) seededBytes.set(sid, b);
618
+ candidateByteMemo.set(sid, b);
615
619
  return b;
616
620
  };
617
- if (diagnostics) diagnostics.phraseScale = seededBytes.size;
621
+
622
+ let framePopulation: Map<number, Uint8Array> | null = null;
623
+
624
+ /** Return the complete phrase-scale candidate population.
625
+ *
626
+ * This is intentionally lazy: queries that never reach frame unanimity
627
+ * keep the cheap per-candidate seed path. Once required, every candidate
628
+ * is bounded by candidateBytes(), loaded at most once, and all subsequent
629
+ * unanimity checks observe the same order-independent population.
630
+ */
631
+ const ensureFramePopulation = (): ReadonlyMap<number, Uint8Array> => {
632
+ if (framePopulation !== null) {
633
+ return framePopulation;
634
+ }
635
+
636
+ const complete = new Map<number, Uint8Array>();
637
+
638
+ for (const sid of candidates) {
639
+ const bytes = bytesOfCandidate(sid);
640
+ if (bytes !== null) {
641
+ complete.set(sid, bytes);
642
+ }
643
+ }
644
+
645
+ framePopulation = complete;
646
+
647
+ if (diagnostics) {
648
+ diagnostics.phraseScale = complete.size;
649
+ }
650
+
651
+ return complete;
652
+ };
618
653
 
619
654
  // FRAME UNANIMITY: a substitution U → C inside the frame (Lf, Rf) is
620
655
  // groundable only when the collected candidates — the store's own sample
@@ -639,7 +674,9 @@ async function bridgeImpl(
639
674
  lf: Uint8Array,
640
675
  rf: Uint8Array,
641
676
  ): boolean => {
642
- for (const bytes of seededBytes.values()) {
677
+ const population = ensureFramePopulation();
678
+
679
+ for (const bytes of population.values()) {
643
680
  let from = 0;
644
681
  for (;;) {
645
682
  const i = indexOf(bytes, lf, from);
package/src/mind/mind.ts CHANGED
@@ -482,8 +482,17 @@ export class Mind implements MindContext {
482
482
  // Inference is a pure function of cumulative bytes. Conversation
483
483
  // boundaries remain persistence/API metadata and must not select a
484
484
  // different mechanism path than respond() on the identical byte stream.
485
- this.answeredSpans = [];
486
- this.currentTurnStart = 0;
485
+ // answeredSpans and currentTurnStart ARE restored from the conversation,
486
+ // however — they are pure functions of the cumulative byte stream (the
487
+ // assistant's own prior replies, and where the current user turn starts,
488
+ // are deterministic given the full transcript). Without them confluence,
489
+ // cover, the weave, and the consensus climb treat prior assistant turns
490
+ // as fresh query content — re-deriving them as constraints, voting
491
+ // anchors, and alignment points.
492
+ this.answeredSpans = conv ? conv.answeredSpans : [];
493
+ this.currentTurnStart = conv && conv.boundaries.length > 0
494
+ ? conv.boundaries[conv.boundaries.length - 1]
495
+ : 0;
487
496
  this.canon = canon ?? null;
488
497
  this.canonMemo = canon ? new Map() : null;
489
498
  this._beginMeter();