@hviana/sema 0.2.0 → 0.2.2
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/geometry.d.ts +28 -0
- package/dist/src/geometry.js +35 -0
- package/dist/src/mind/attention.js +29 -0
- package/dist/src/mind/graph-search.js +127 -47
- package/dist/src/mind/learning.d.ts +1 -1
- package/dist/src/mind/learning.js +20 -5
- package/dist/src/mind/mechanisms/cover.js +32 -0
- package/dist/src/mind/mechanisms/recall.js +26 -2
- package/dist/src/mind/mind.d.ts +2 -2
- package/dist/src/mind/mind.js +22 -17
- package/dist/src/mind/primitives.d.ts +16 -9
- package/dist/src/mind/primitives.js +58 -22
- package/dist/src/mind/types.d.ts +32 -10
- package/package.json +1 -1
- package/src/geometry.ts +61 -1
- package/src/mind/attention.ts +38 -0
- package/src/mind/graph-search.ts +128 -46
- package/src/mind/learning.ts +20 -3
- package/src/mind/mechanisms/cover.ts +38 -0
- package/src/mind/mechanisms/recall.ts +30 -2
- package/src/mind/mind.ts +31 -23
- package/src/mind/primitives.ts +71 -29
- package/src/mind/types.ts +33 -10
package/dist/src/geometry.d.ts
CHANGED
|
@@ -90,6 +90,13 @@ export declare function consensusFloor(N: number): number;
|
|
|
90
90
|
* replaces runtime coverage gating with a batch pass that removes
|
|
91
91
|
* structurally-isolated entries. Derived, never tuned. */
|
|
92
92
|
export declare function coverageBar(_maxGroup: number, D: number): number;
|
|
93
|
+
export interface Folded {
|
|
94
|
+
tree: Sema;
|
|
95
|
+
/** Byte length of the subtree — carried incrementally so the stable-prefix
|
|
96
|
+
* boundary scan never re-walks subtrees (the old per-level walk was
|
|
97
|
+
* O(n log n) over the whole input). */
|
|
98
|
+
len: number;
|
|
99
|
+
}
|
|
93
100
|
export interface Grid {
|
|
94
101
|
width: number;
|
|
95
102
|
height: number;
|
|
@@ -116,6 +123,27 @@ export declare function knownPrefixLength(bytes: Uint8Array, leafAt: (i: number)
|
|
|
116
123
|
* turn extend perception instead of refolding it: identical prefixes
|
|
117
124
|
* produce identical subtrees regardless of what follows them. */
|
|
118
125
|
export declare function bytesToTree(space: Space, alphabet: Alphabet, bytes: Uint8Array, leafAt?: (i: number) => number | null, lookup?: (leafIds: number[]) => number | null, boundaries?: readonly number[]): Sema;
|
|
126
|
+
/** A stable-prefix fold's reusable state: the segment edge offsets and each
|
|
127
|
+
* segment's independently-folded root ({@link riverFoldRaw} output). A
|
|
128
|
+
* grown stream whose boundary set EXTENDS a previous fold's reuses every
|
|
129
|
+
* matching segment's Folded unchanged (segments fold independently by
|
|
130
|
+
* construction, so reuse is bit-identical to refolding) and folds only the
|
|
131
|
+
* new right-edge segment — O(turn) per extension, the stable-prefix
|
|
132
|
+
* counterpart of {@link FoldPyramid}. Purely a cache: the produced tree
|
|
133
|
+
* never depends on cache state. */
|
|
134
|
+
export interface StableFold {
|
|
135
|
+
edges: number[];
|
|
136
|
+
segs: Folded[];
|
|
137
|
+
}
|
|
138
|
+
/** {@link stablePrefixFold} with incremental segment reuse — same cuts, same
|
|
139
|
+
* segment folds, same left-nested join, same single root normalize; `prev`
|
|
140
|
+
* only elides recomputing segments whose [start,end) offsets it already
|
|
141
|
+
* folded over a byte-identical prefix (the caller keys the cache by
|
|
142
|
+
* content). Requires a non-empty effective boundary set. */
|
|
143
|
+
export declare function stablePrefixFoldIncremental(space: Space, alphabet: Alphabet, bytes: Uint8Array, boundaries: readonly number[], prev?: StableFold): {
|
|
144
|
+
tree: Sema;
|
|
145
|
+
fold: StableFold;
|
|
146
|
+
};
|
|
119
147
|
/** The PLAIN fold's full level pyramid — every level's item list, bottom
|
|
120
148
|
* (leaves) to top (root). Left-grouped folding is RADIX-ALIGNED: the item
|
|
121
149
|
* at level L, index i, covers exactly bytes [i·mg^L, (i+1)·mg^L) whenever
|
package/dist/src/geometry.js
CHANGED
|
@@ -321,6 +321,41 @@ function stablePrefixFold(space, alphabet, bytes, boundaries) {
|
|
|
321
321
|
normalize(cur.tree.v);
|
|
322
322
|
return cur.tree;
|
|
323
323
|
}
|
|
324
|
+
/** {@link stablePrefixFold} with incremental segment reuse — same cuts, same
|
|
325
|
+
* segment folds, same left-nested join, same single root normalize; `prev`
|
|
326
|
+
* only elides recomputing segments whose [start,end) offsets it already
|
|
327
|
+
* folded over a byte-identical prefix (the caller keys the cache by
|
|
328
|
+
* content). Requires a non-empty effective boundary set. */
|
|
329
|
+
export function stablePrefixFoldIncremental(space, alphabet, bytes, boundaries, prev) {
|
|
330
|
+
const cuts = [];
|
|
331
|
+
let prevB = 0;
|
|
332
|
+
for (const b of boundaries) {
|
|
333
|
+
if (b > prevB && b < bytes.length) {
|
|
334
|
+
cuts.push(b);
|
|
335
|
+
prevB = b;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const edges = [0, ...cuts, bytes.length];
|
|
339
|
+
const segs = [];
|
|
340
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
341
|
+
const hit = prev !== undefined && prev.edges[i] === edges[i] &&
|
|
342
|
+
prev.edges[i + 1] === edges[i + 1]
|
|
343
|
+
? prev.segs[i]
|
|
344
|
+
: undefined;
|
|
345
|
+
segs.push(hit ??
|
|
346
|
+
riverFoldRaw(space, bytesToLeaves(alphabet, bytes.subarray(edges[i], edges[i + 1]))));
|
|
347
|
+
}
|
|
348
|
+
if (segs.length === 1) {
|
|
349
|
+
// Degenerate boundary set — the plain fold, as stablePrefixFold does.
|
|
350
|
+
const tree = riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
|
|
351
|
+
return { tree, fold: { edges, segs } };
|
|
352
|
+
}
|
|
353
|
+
let cur = segs[0];
|
|
354
|
+
for (let i = 1; i < segs.length; i++)
|
|
355
|
+
cur = fold2(space, cur, segs[i]);
|
|
356
|
+
normalize(cur.tree.v);
|
|
357
|
+
return { tree: cur.tree, fold: { edges, segs } };
|
|
358
|
+
}
|
|
324
359
|
/** Join two folded items as one 2-kid branch — the top-level join of the
|
|
325
360
|
* stable-prefix fold, identical FP ops to foldSlice's seat-bound
|
|
326
361
|
* accumulation over a group of two. Unnormalized (interior). */
|
|
@@ -736,6 +736,27 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
|
|
|
736
736
|
continue;
|
|
737
737
|
if (ra.end >= rb.start)
|
|
738
738
|
continue; // overlap or adjacent — nothing between
|
|
739
|
+
// Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
|
|
740
|
+
// that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
|
|
741
|
+
// genuine, individually-corroborated evidence about what fills the gap
|
|
742
|
+
// — gate the container search below: a joint container is binding
|
|
743
|
+
// evidence only when it is CONSISTENT with that evidence, i.e. its own
|
|
744
|
+
// bytes actually contain what the between-region says. This is the
|
|
745
|
+
// n-ary composition's normal shape (a between-attribute's bytes DO
|
|
746
|
+
// recur inside the joint container, credited as an "extra" below) as
|
|
747
|
+
// opposed to a container that silently substitutes something else for
|
|
748
|
+
// it (e.g. bridging past "Italy" to a container whose interior is
|
|
749
|
+
// "Japan" — a different, contradicting learnt whole).
|
|
750
|
+
// Only a KNOWN (content-addressed, exact) between-region qualifies —
|
|
751
|
+
// an approximate region's resonance climbing "somewhere" is ordinary
|
|
752
|
+
// noise (any ANN query returns SOME nearest neighbour), not evidence
|
|
753
|
+
// this specific gap already means something specific.
|
|
754
|
+
const between = [];
|
|
755
|
+
for (let m = a + 1; m < b; m++) {
|
|
756
|
+
if (strong.has(cand[m]) && !consumed.has(cand[m]) &&
|
|
757
|
+
regions[cand[m]].known)
|
|
758
|
+
between.push(cand[m]);
|
|
759
|
+
}
|
|
739
760
|
// A single KNOWN region covering both: the whole form is already a
|
|
740
761
|
// stored identity that votes directly; its pieces add nothing.
|
|
741
762
|
if (regions.some((r) => r.known && r.start <= ra.start && rb.end <= r.end))
|
|
@@ -781,6 +802,14 @@ async function crossRegionVotes(ctx, query, regions, rvs, k, N, reachMemo) {
|
|
|
781
802
|
if (indexOf(query, joined, 0) >= 0)
|
|
782
803
|
continue; // query says it itself
|
|
783
804
|
}
|
|
805
|
+
// CONTRADICTION GUARD: a between-region already carrying its own
|
|
806
|
+
// vote must actually recur in this container's bytes — otherwise
|
|
807
|
+
// the container is a different learnt whole that happens to share
|
|
808
|
+
// ra/rb, and letting it stand in for the gap would silently
|
|
809
|
+
// override evidence the query itself already resolved there.
|
|
810
|
+
if (between.some((bi) => indexOf(bytes, query.subarray(regions[bi].start, regions[bi].end), 0) < 0)) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
784
813
|
let cov = left.length + right.length;
|
|
785
814
|
const extras = [];
|
|
786
815
|
for (const ei of cand) {
|
|
@@ -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.
|
|
@@ -18,7 +18,7 @@ export declare function internTreeIds(ctx: MindContext, node: Sema, ids: Map<Sem
|
|
|
18
18
|
export declare function indexSubSpans(ctx: MindContext, tree: Sema, ids: Map<Sema, number>): Promise<boolean>;
|
|
19
19
|
/** Perceive, intern, and index a single input. Returns the perceived tree,
|
|
20
20
|
* root id, id map, and the changed (new) subtrees for halo reinforcement. */
|
|
21
|
-
export declare function deposit(ctx: MindContext, input: Input, track: boolean): Promise<{
|
|
21
|
+
export declare function deposit(ctx: MindContext, input: Input, track: boolean, conversational?: boolean): Promise<{
|
|
22
22
|
tree: Sema;
|
|
23
23
|
rootId: number;
|
|
24
24
|
ids: Map<Sema, number>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// node. A fact is an EDGE between node ids; recall traverses edges.
|
|
5
5
|
import { bindSeat, companySignature, isChunk } from "../sema.js";
|
|
6
6
|
import { changedNodes } from "./types.js";
|
|
7
|
-
import { inputBytes, perceiveDeposit, resolve, } from "./primitives.js";
|
|
7
|
+
import { inputBytes, latin1Key, perceiveDeposit, resolve, } from "./primitives.js";
|
|
8
8
|
import { canonicalWindows, leafIdPrefix } from "./canonical.js";
|
|
9
9
|
import { fold as foldVecs } from "../sema.js";
|
|
10
10
|
/** Intern a perceived tree into node ids, bottom-up, sharing equal subtrees.
|
|
@@ -103,7 +103,7 @@ export async function indexSubSpans(ctx, tree, ids) {
|
|
|
103
103
|
}
|
|
104
104
|
/** Perceive, intern, and index a single input. Returns the perceived tree,
|
|
105
105
|
* root id, id map, and the changed (new) subtrees for halo reinforcement. */
|
|
106
|
-
export async function deposit(ctx, input, track) {
|
|
106
|
+
export async function deposit(ctx, input, track, conversational = false) {
|
|
107
107
|
const bytes = inputBytes(ctx, input);
|
|
108
108
|
// Deposit-shaped perception: stable-prefix tree SEEDING (see
|
|
109
109
|
// perceiveDeposit) — an accumulated context re-folds only its new suffix,
|
|
@@ -111,8 +111,13 @@ export async function deposit(ctx, input, track) {
|
|
|
111
111
|
// (no store-probe fallback): a knownPrefixLength scan on every novel fact
|
|
112
112
|
// would cost O(n²) hashing, while conversation replays are always warm —
|
|
113
113
|
// re-deposition replays from the first turn, rebuilding the cache as it
|
|
114
|
-
// goes.
|
|
115
|
-
|
|
114
|
+
// goes. `conversational` scopes the STABLE-PREFIX variant (turn-boundary
|
|
115
|
+
// folding, matching query-time perception) to ingestPair's own growing
|
|
116
|
+
// context argument — a bare ingestOne deposit whose bytes merely happen
|
|
117
|
+
// to extend an earlier UNRELATED deposit (no conversational relationship)
|
|
118
|
+
// must keep the plain fold, or two coincidentally-prefix-sharing facts
|
|
119
|
+
// would stop sharing structure with each other.
|
|
120
|
+
const tree = perceiveDeposit(ctx, bytes, conversational);
|
|
116
121
|
const ids = new Map();
|
|
117
122
|
const rootId = await internTreeIds(ctx, tree, ids);
|
|
118
123
|
const indexed = await indexSubSpans(ctx, tree, ids);
|
|
@@ -185,9 +190,19 @@ async function propagateSuffixes(ctx, src, dst) {
|
|
|
185
190
|
}
|
|
186
191
|
/** Ingest a pair (context, continuation) — learn an edge and pour halos. */
|
|
187
192
|
export async function ingestPair(ctx, ctxInput, cont) {
|
|
188
|
-
const c = await deposit(ctx, ctxInput, true);
|
|
193
|
+
const c = await deposit(ctx, ctxInput, true, true);
|
|
189
194
|
const cont_ = await deposit(ctx, cont, false);
|
|
190
195
|
const ctxId = c.rootId, contId = cont_.rootId;
|
|
196
|
+
// Stamp this turn's continuation onto its own cache entry — the proof a
|
|
197
|
+
// FUTURE, longer ctxInput needs (see perceiveDeposit) to recognise itself
|
|
198
|
+
// as this conversation's genuine next turn rather than an unrelated fact
|
|
199
|
+
// that happens to share this ctxInput's byte prefix.
|
|
200
|
+
{
|
|
201
|
+
const ctxBytes = inputBytes(ctx, ctxInput);
|
|
202
|
+
const entry = ctx._depositTrees.get(latin1Key(ctxBytes));
|
|
203
|
+
if (entry !== undefined)
|
|
204
|
+
entry.nextBytes = inputBytes(ctx, cont);
|
|
205
|
+
}
|
|
191
206
|
await ctx.store.link(ctxId, contId);
|
|
192
207
|
await propagateSuffixes(ctx, ctxId, contId);
|
|
193
208
|
// Halos pour company SIGNATURES (identity), not gists (content) — see
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// cover runs FIRST in defaultMechanisms: a computed-backed cover becomes a
|
|
9
9
|
// near-zero-cost incumbent that prunes the other mechanisms through the
|
|
10
10
|
// ordinary admissible-floor check, with no extension special-case anywhere.
|
|
11
|
+
import { bytesEqual, indexOf } from "../../bytes.js";
|
|
11
12
|
import { read, resolve } from "../primitives.js";
|
|
12
13
|
import { guidedFirst } from "../traverse.js";
|
|
13
14
|
import { conceptHop } from "../match.js";
|
|
@@ -158,6 +159,37 @@ export const coverMechanism = {
|
|
|
158
159
|
: "lightest derivation: the chosen spans, left to right");
|
|
159
160
|
if (segs === null)
|
|
160
161
|
return [];
|
|
162
|
+
// A chosen span's SUBSTITUTED bytes (an edge followed from a recognised
|
|
163
|
+
// site, not the site's own literal text read back) that equal a byte
|
|
164
|
+
// span the query ALREADY CONTAINS elsewhere restates part of the
|
|
165
|
+
// question — never an answer (the same principle recall.ts's tiers
|
|
166
|
+
// apply to a whole-query projection: "a projection that is a proper
|
|
167
|
+
// byte-subspan of the query restates part of the question"). A
|
|
168
|
+
// recognised site that is itself an entire PRIOR TURN of a multi-turn
|
|
169
|
+
// query is exactly this shape: it carries a genuine learnt
|
|
170
|
+
// continuation, but that continuation is something the asker already
|
|
171
|
+
// said moments later in the SAME query, not a new answer — the cover
|
|
172
|
+
// search has no notion of "turn" to gate this itself, so the check
|
|
173
|
+
// belongs here, over the derivation it already chose.
|
|
174
|
+
const W = ctx.space.maxGroup;
|
|
175
|
+
for (const s of segs) {
|
|
176
|
+
if (!s.rec)
|
|
177
|
+
continue;
|
|
178
|
+
const literal = s.j - s.i === s.bytes.length &&
|
|
179
|
+
bytesEqual(s.bytes, query.subarray(s.i, s.j));
|
|
180
|
+
if (literal)
|
|
181
|
+
continue;
|
|
182
|
+
// A span shorter than one river window is exactly the "ice"/"c"
|
|
183
|
+
// case: a chain hop's short terminal coincidentally recurring as a
|
|
184
|
+
// substring of an unrelated longer word. Below the quantum, byte
|
|
185
|
+
// overlap is chance, not evidence — the same floor identityBar and
|
|
186
|
+
// reachThreshold hold every other structural-overlap claim to.
|
|
187
|
+
if (s.bytes.length >= W && s.bytes.length < query.length &&
|
|
188
|
+
indexOf(query, s.bytes, 0) >= 0) {
|
|
189
|
+
ctx.trace?.step("restatedSpan", [rItem(s.bytes, "substituted", s.node, [s.i, s.j])], [], "the chosen span's substitution already occurs elsewhere in the query — restates it, not an answer");
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
161
193
|
const composed = liftAnswer(segs, query.length);
|
|
162
194
|
if (composed === null)
|
|
163
195
|
return [];
|
|
@@ -74,10 +74,34 @@ export async function recallByResonance(ctx, query, pre) {
|
|
|
74
74
|
maximal.push(s);
|
|
75
75
|
maxEnd = s.end;
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
// The wrapper must actually BE scaffolding: RC8's own premise is "the
|
|
78
|
+
// wrapper is scaffolding; the argument is the span that leads
|
|
79
|
+
// somewhere" ("How do you say 'thank you' in French?" — everything
|
|
80
|
+
// outside the argument is a small fixed template). When the query
|
|
81
|
+
// instead has ANOTHER substantial recognised form (≥ W2, the same
|
|
82
|
+
// constituent bar the argument itself must clear) sitting OUTSIDE the
|
|
83
|
+
// chosen argument, the query is not one argument in a wrapper — it is
|
|
84
|
+
// several complete, independently-meaningful pieces (a multi-turn
|
|
85
|
+
// conversation's own accumulated turns are exactly this shape), and
|
|
86
|
+
// binding to the argument's continuation would answer past content
|
|
87
|
+
// the query itself already carries forward. Derived from the same W2
|
|
88
|
+
// bar the argument itself is held to, never a separate tuned number.
|
|
89
|
+
const hasSubstantialOutside = maximal.length === 1 &&
|
|
90
|
+
pre.rec.sites.some((s) => s.end - s.start >= W2 &&
|
|
91
|
+
(s.end <= maximal[0].start || s.start >= maximal[0].end));
|
|
92
|
+
if (maximal.length === 1 && !hasSubstantialOutside) {
|
|
78
93
|
const arg = maximal[0];
|
|
79
94
|
const g = await follow(ctx, arg.payload, queryGist);
|
|
80
|
-
|
|
95
|
+
// The same "no restated fragment" guard tier 2 applies below (§ "the
|
|
96
|
+
// anchor cleared the consensus floor..."): a followed continuation
|
|
97
|
+
// that is itself a proper byte-subspan of the QUERY restates part of
|
|
98
|
+
// the question — never an answer. A multi-turn query's own later
|
|
99
|
+
// turns are exact, content-addressed matches for exactly this reason
|
|
100
|
+
// (each turn is its own previously-learnt form), so without this
|
|
101
|
+
// guard the argument's OWN later restatement in the same
|
|
102
|
+
// conversation reads as if it were the next thing to say.
|
|
103
|
+
if (g !== null && g.length > 0 &&
|
|
104
|
+
!(g.length < query.length && indexOf(query, g, 0) >= 0)) {
|
|
81
105
|
return ground(g, "argument binding — the query's sole edge-source constituent, continuation followed", [[arg.start, arg.end]], STEP);
|
|
82
106
|
}
|
|
83
107
|
}
|
package/dist/src/mind/mind.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Vec } from "../vec.js";
|
|
2
2
|
import { Sema, Space } from "../sema.js";
|
|
3
3
|
import { Alphabet } from "../alphabet.js";
|
|
4
|
-
import {
|
|
4
|
+
import { Grid } from "../geometry.js";
|
|
5
5
|
import { BoundedMap, type Store } from "../store.js";
|
|
6
6
|
import { type MindConfig } from "../config.js";
|
|
7
7
|
import { type Canon } from "../canon.js";
|
|
@@ -111,7 +111,7 @@ export declare class Mind implements MindContext {
|
|
|
111
111
|
* {@link MindContext._gistCache}. 32 MB ≈ 8K gists at D=1024; hub
|
|
112
112
|
* candidate sets (√N at most) fit comfortably and recur across queries. */
|
|
113
113
|
_gistCache: BoundedMap<number, Vec>;
|
|
114
|
-
_depositTrees: BoundedMap<string,
|
|
114
|
+
_depositTrees: BoundedMap<string, import("./types.js").DepositCacheEntry>;
|
|
115
115
|
_depositLens: Set<number>;
|
|
116
116
|
_internIds: WeakMap<Sema, number>;
|
|
117
117
|
private _nextConvId;
|
package/dist/src/mind/mind.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Implementation split across src/mind/*.ts — this file assembles the Mind class.
|
|
11
11
|
import { makeKeyring, rng, setVecConfig } from "../vec.js";
|
|
12
12
|
import { Alphabet } from "../alphabet.js";
|
|
13
|
-
import {
|
|
13
|
+
import { bytesToTree, reachThreshold, } from "../geometry.js";
|
|
14
14
|
import { BoundedMap } from "../store.js";
|
|
15
15
|
import { SQliteStore } from "../store-sqlite.js";
|
|
16
16
|
import { resolveConfig } from "../config.js";
|
|
@@ -263,14 +263,13 @@ export class Mind {
|
|
|
263
263
|
* where one turn ends and the next begins. */
|
|
264
264
|
beginConversation(state) {
|
|
265
265
|
const id = this._nextConvId++;
|
|
266
|
-
// Build the initial pyramid: from scratch for a new conversation,
|
|
267
|
-
// or from the saved context bytes when restoring.
|
|
268
266
|
const initBytes = state?.context ?? new Uint8Array(0);
|
|
269
|
-
const
|
|
267
|
+
const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
|
|
268
|
+
const tree = bytesToTree(this.space, this.alphabet, initBytes, undefined, undefined, initBoundaries.length > 0 ? initBoundaries : undefined);
|
|
270
269
|
this._conversations.set(id, {
|
|
271
|
-
|
|
270
|
+
tree,
|
|
272
271
|
bytes: initBytes,
|
|
273
|
-
boundaries:
|
|
272
|
+
boundaries: initBoundaries,
|
|
274
273
|
perceiveMemo: new Map(),
|
|
275
274
|
recogniseMemo: new Map(),
|
|
276
275
|
climbMemo: new Map(),
|
|
@@ -316,21 +315,18 @@ export class Mind {
|
|
|
316
315
|
* place a context grows ({@link addTurn} and {@link respondTurn} both
|
|
317
316
|
* come through here), so the append semantics cannot drift. */
|
|
318
317
|
_growContext(data, turnBytes) {
|
|
319
|
-
const prevLen = data.
|
|
318
|
+
const prevLen = data.bytes.length;
|
|
320
319
|
// An empty turn neither grows the context nor marks a boundary —
|
|
321
320
|
// boundaries are documented strictly increasing, and a zero-length
|
|
322
|
-
// "turn" is no turn.
|
|
323
|
-
// refold below O(1), so the existing tree is returned unchanged.
|
|
321
|
+
// "turn" is no turn. Nothing changed, so the existing tree stands.
|
|
324
322
|
const grow = turnBytes.length > 0;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
: turnBytes;
|
|
330
|
-
if (grow && prevLen > 0)
|
|
323
|
+
if (!grow)
|
|
324
|
+
return data.tree;
|
|
325
|
+
const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
|
|
326
|
+
if (prevLen > 0)
|
|
331
327
|
data.boundaries.push(prevLen);
|
|
332
|
-
const
|
|
333
|
-
data.
|
|
328
|
+
const tree = bytesToTree(this.space, this.alphabet, grown, undefined, undefined, data.boundaries.length > 0 ? data.boundaries : undefined);
|
|
329
|
+
data.tree = tree;
|
|
334
330
|
data.bytes = grown;
|
|
335
331
|
data.perceiveMemo.set(latin1Key(grown), tree);
|
|
336
332
|
return tree;
|
|
@@ -377,6 +373,15 @@ export class Mind {
|
|
|
377
373
|
this.canon = this._canonFor(typeof turn === "string" ? textCanon : null);
|
|
378
374
|
this.canonMemo = this.canon ? new Map() : null;
|
|
379
375
|
try {
|
|
376
|
+
// No recognise-memo pre-seeding here: that used to be necessary
|
|
377
|
+
// because the flat/positional fold lost visibility into an earlier
|
|
378
|
+
// turn's own structure once later bytes shifted its position
|
|
379
|
+
// (foldTree no longer visited the turn's root node). The
|
|
380
|
+
// STABLE-PREFIX fold (see {@link ConversationData}) makes every
|
|
381
|
+
// turn's subtree independent of what follows it by construction, so
|
|
382
|
+
// recognise() finds it correctly on its own, first-touch, exactly
|
|
383
|
+
// once per turn — no workaround needed, and none pre-empting
|
|
384
|
+
// recognise()'s own memo with a partial result.
|
|
380
385
|
const top = this.trace?.enter("respondTurn", [
|
|
381
386
|
rItem(newContext, "query"),
|
|
382
387
|
]);
|
|
@@ -15,15 +15,22 @@ export declare function latin1Key(bytes: Uint8Array): string;
|
|
|
15
15
|
* CALLER — who assembled the multi-turn context — knows where those
|
|
16
16
|
* boundaries are; the geometry never guesses them from the bytes. */
|
|
17
17
|
export declare function perceive(ctx: MindContext, input: Input, leafAt?: (i: number) => number | null, lookup?: (ids: number[]) => number | null, boundaries?: readonly number[]): Sema;
|
|
18
|
-
/** The DEPOSIT-shaped perceive
|
|
19
|
-
* perception
|
|
20
|
-
* for exact recall),
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
18
|
+
/** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
|
|
19
|
+
* (bit-identical to inference perception of a standalone query — that
|
|
20
|
+
* structural train/inference agreement is load-bearing for exact recall),
|
|
21
|
+
* computed incrementally via the fold's level pyramid
|
|
22
|
+
* ({@link bytesToTreePyramid}). An input that EXTENDS a previously
|
|
23
|
+
* deposited one is a conversation context grown by one turn — the cached
|
|
24
|
+
* prefix length IS the turn boundary (derived from the deposit sequence
|
|
25
|
+
* itself, never from content conventions) — and takes the STABLE-PREFIX
|
|
26
|
+
* fold over the accumulated boundaries, bit-identical to the boundary
|
|
27
|
+
* fold query-time conversation perception uses, so the trained context
|
|
28
|
+
* node and the query's context subtree are the SAME node. Segment folds
|
|
29
|
+
* reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
|
|
30
|
+
* instead of O(context) per turn. The fold state is purely a cache; the
|
|
31
|
+
* boundary accumulation is what an evicted chain loses (falling back to
|
|
32
|
+
* the plain fold, the pre-boundary shape — a warm replay restores it). */
|
|
33
|
+
export declare function perceiveDeposit(ctx: MindContext, bytes: Uint8Array, conversational?: boolean): Sema;
|
|
27
34
|
/** The raw bytes of an input — modality-neutral conversion. */
|
|
28
35
|
export declare function inputBytes(ctx: MindContext, input: Input): Uint8Array;
|
|
29
36
|
/** Convenience: the gist vector of a byte span. */
|