@hviana/sema 0.1.6 → 0.1.8
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/example/train_base.js +42 -9
- package/dist/src/alu/src/parser.d.ts +9 -0
- package/dist/src/alu/src/parser.js +110 -2
- package/dist/src/canon.d.ts +26 -0
- package/dist/src/canon.js +57 -0
- package/dist/src/geometry.d.ts +13 -2
- package/dist/src/geometry.js +101 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/mind/attention.js +11 -7
- package/dist/src/mind/learning.js +33 -1
- package/dist/src/mind/match.d.ts +4 -2
- package/dist/src/mind/match.js +30 -6
- package/dist/src/mind/mechanisms/cast.js +18 -4
- package/dist/src/mind/mechanisms/confluence.js +13 -1
- package/dist/src/mind/mechanisms/recall.js +115 -31
- package/dist/src/mind/mind.d.ts +138 -12
- package/dist/src/mind/mind.js +284 -22
- package/dist/src/mind/primitives.d.ts +22 -2
- package/dist/src/mind/primitives.js +95 -6
- package/dist/src/mind/recognition.js +31 -8
- package/dist/src/mind/traverse.d.ts +13 -0
- package/dist/src/mind/traverse.js +41 -0
- package/dist/src/mind/types.d.ts +33 -21
- package/dist/src/store-sqlite.d.ts +10 -0
- package/dist/src/store-sqlite.js +58 -0
- package/dist/src/store.d.ts +23 -0
- package/dist/src/store.js +13 -2
- package/example/train_base.ts +49 -9
- package/index.html +65 -0
- package/package.json +1 -1
- package/src/alu/src/parser.ts +105 -4
- package/src/canon.ts +65 -0
- package/src/geometry.ts +105 -1
- package/src/index.ts +1 -0
- package/src/mind/attention.ts +11 -6
- package/src/mind/learning.ts +39 -1
- package/src/mind/match.ts +29 -6
- package/src/mind/mechanisms/cast.ts +21 -6
- package/src/mind/mechanisms/confluence.ts +15 -1
- package/src/mind/mechanisms/recall.ts +132 -41
- package/src/mind/mind.ts +396 -22
- package/src/mind/primitives.ts +109 -7
- package/src/mind/recognition.ts +36 -8
- package/src/mind/traverse.ts +47 -0
- package/src/mind/types.ts +30 -21
- package/src/store-sqlite.ts +76 -0
- package/src/store.ts +46 -2
- package/test/13-conversation.test.mjs +240 -20
- package/test/35-prefix-edge.test.mjs +86 -0
package/src/alu/src/parser.ts
CHANGED
|
@@ -172,10 +172,27 @@ export class QueryParser {
|
|
|
172
172
|
j,
|
|
173
173
|
bytes: this.evalRun(query.subarray(i, j)),
|
|
174
174
|
}));
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
|
|
175
|
+
// BRACKETED runs — "(3 + 4) * (10 - 2)" — extend across grouping
|
|
176
|
+
// notation the flat alternation cannot cross. The expression grammar
|
|
177
|
+
// (expr.ts) already nests and parenthesizes; this only hands it the
|
|
178
|
+
// WHOLE bracketed span instead of the fragments between brackets. A
|
|
179
|
+
// bracketed run that evaluates ABSORBS the flat runs inside it (the
|
|
180
|
+
// authority law: contained spans are its material); one that does not
|
|
181
|
+
// evaluate leaves the flat readings untouched.
|
|
182
|
+
const bracketed = this.bracketedRuns(query, tokens)
|
|
183
|
+
.map(([i, j]) => ({ i, j, bytes: this.evalRun(query.subarray(i, j)) }))
|
|
184
|
+
.filter((r): r is ComputedSpan => r.bytes !== null);
|
|
185
|
+
const absorbed = (r: { i: number; j: number }) =>
|
|
186
|
+
bracketed.some((b) => b.i <= r.i && r.j <= b.j);
|
|
187
|
+
const composedRuns = [
|
|
188
|
+
...runs.filter((r) => !absorbed(r)),
|
|
189
|
+
...bracketed,
|
|
190
|
+
].sort((a, b) => a.i - b.i);
|
|
191
|
+
const spans: ComputedSpan[] = [
|
|
192
|
+
...runs.filter((r): r is ComputedSpan => r.bytes !== null),
|
|
193
|
+
...bracketed,
|
|
194
|
+
];
|
|
195
|
+
let stream = compose(tokens, composedRuns);
|
|
179
196
|
const readings = new Map<Token, readonly string[]>();
|
|
180
197
|
for (;;) {
|
|
181
198
|
const fired = await this.operations(query, stream, readings);
|
|
@@ -302,6 +319,90 @@ export class QueryParser {
|
|
|
302
319
|
return runs;
|
|
303
320
|
}
|
|
304
321
|
|
|
322
|
+
/** The maximal BRACKETED arithmetic runs: like {@link arithmeticRuns} but
|
|
323
|
+
* grouping brackets participate — an all-'(' term opens depth where an
|
|
324
|
+
* operand is expected, a term beginning with ')' closes it where an
|
|
325
|
+
* operator could follow (only its bracket prefix joins the run; anything
|
|
326
|
+
* after — a trailing "?" — ends it). A run is recorded only when it is
|
|
327
|
+
* BALANCED, actually crossed a bracket, and contains an operator — the
|
|
328
|
+
* bracket-free case is {@link arithmeticRuns}' own. Evaluation is the
|
|
329
|
+
* same registry-derived grammar, whose lexer already reads the brackets. */
|
|
330
|
+
private bracketedRuns(
|
|
331
|
+
query: Uint8Array,
|
|
332
|
+
tokens: Token[],
|
|
333
|
+
): Array<[number, number]> {
|
|
334
|
+
const OPEN = 0x28, CLOSE = 0x29;
|
|
335
|
+
const bracketPrefix = (t: Token, b: number): number => {
|
|
336
|
+
if (t.kind !== "term") return 0;
|
|
337
|
+
let n = 0;
|
|
338
|
+
while (t.i + n < t.j && query[t.i + n] === b) n++;
|
|
339
|
+
return n;
|
|
340
|
+
};
|
|
341
|
+
const allOf = (t: Token, b: number): boolean =>
|
|
342
|
+
bracketPrefix(t, b) === t.j - t.i;
|
|
343
|
+
const bridged = (from: number, to: number): boolean =>
|
|
344
|
+
from >= to || this.host.segment(query.subarray(from, to)).length <= 1;
|
|
345
|
+
const runs: Array<[number, number]> = [];
|
|
346
|
+
let k = 0;
|
|
347
|
+
while (k < tokens.length) {
|
|
348
|
+
const first = tokens[k];
|
|
349
|
+
if (first.kind !== "operand" && !allOf(first, OPEN)) {
|
|
350
|
+
k++;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
let depth = 0;
|
|
354
|
+
let ops = 0;
|
|
355
|
+
let brackets = 0;
|
|
356
|
+
let expectOperand = true;
|
|
357
|
+
let end = -1; // byte end of the last VALID run state (balanced, after operand/close)
|
|
358
|
+
let endTok = k; // token index just past the accepted prefix
|
|
359
|
+
let m = k;
|
|
360
|
+
let prevJ = -1;
|
|
361
|
+
while (m < tokens.length) {
|
|
362
|
+
const t = tokens[m];
|
|
363
|
+
if (prevJ >= 0 && !bridged(prevJ, t.i)) break;
|
|
364
|
+
if (expectOperand) {
|
|
365
|
+
if (allOf(t, OPEN)) {
|
|
366
|
+
depth += t.j - t.i;
|
|
367
|
+
brackets++;
|
|
368
|
+
} else if (t.kind === "operand") {
|
|
369
|
+
expectOperand = false;
|
|
370
|
+
if (depth === 0) {
|
|
371
|
+
end = t.j;
|
|
372
|
+
endTok = m + 1;
|
|
373
|
+
}
|
|
374
|
+
} else break;
|
|
375
|
+
} else {
|
|
376
|
+
const c = bracketPrefix(t, CLOSE);
|
|
377
|
+
if (c > 0 && depth > 0) {
|
|
378
|
+
const take = Math.min(c, depth);
|
|
379
|
+
depth -= take;
|
|
380
|
+
brackets++;
|
|
381
|
+
if (depth === 0) {
|
|
382
|
+
end = t.i + take;
|
|
383
|
+
endTok = m + 1;
|
|
384
|
+
}
|
|
385
|
+
// A term with content beyond its usable bracket prefix ("?")
|
|
386
|
+
// ends the run inside the term.
|
|
387
|
+
if (take < t.j - t.i) break;
|
|
388
|
+
} else if (t.kind === "operator") {
|
|
389
|
+
expectOperand = true;
|
|
390
|
+
ops++;
|
|
391
|
+
} else break;
|
|
392
|
+
}
|
|
393
|
+
prevJ = t.j;
|
|
394
|
+
m++;
|
|
395
|
+
}
|
|
396
|
+
if (end >= 0 && ops > 0 && brackets > 0) {
|
|
397
|
+
runs.push([first.i, end]);
|
|
398
|
+
k = endTok;
|
|
399
|
+
} else {
|
|
400
|
+
k++;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return runs;
|
|
404
|
+
}
|
|
405
|
+
|
|
305
406
|
/** Evaluate an infix-arithmetic run to its canonical result bytes, through
|
|
306
407
|
* the kernel's recursive expression evaluator, or null if it does not
|
|
307
408
|
* evaluate. A whole result stays an exact int; otherwise the canonical
|
package/src/canon.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// canon.ts — content canonicalization for equivalence-class resolution.
|
|
2
|
+
//
|
|
3
|
+
// The store is content-addressed on RAW bytes: "What", "WHAT" and "what" are
|
|
4
|
+
// three different hashes, so a query whose surface form varies from the
|
|
5
|
+
// trained form resolves to nothing even though the CONTENT is the same. A
|
|
6
|
+
// CANONICALIZER maps every surface variant of the same content onto one
|
|
7
|
+
// canonical byte string; the store keeps a small hash index from canonical
|
|
8
|
+
// keys to node ids (see Store.canonAdd/canonFind), and resolution falls back
|
|
9
|
+
// to that index when the exact content-addressed lookup misses.
|
|
10
|
+
//
|
|
11
|
+
// The canonicalizer is MODALITY-SPECIFIC and always INJECTED — nothing in the
|
|
12
|
+
// store or the mind's core knows what "case" or "whitespace" is. The text
|
|
13
|
+
// canonicalizer below is the one `respondText`/`respondTurnText` pass down;
|
|
14
|
+
// a grid or audio modality would supply its own (or none).
|
|
15
|
+
//
|
|
16
|
+
// Canonical keys are equivalence-class LABELS, never content: they are hashed
|
|
17
|
+
// and verified (canon(stored bytes) must equal canon(query bytes) before an
|
|
18
|
+
// id is accepted), so a hash collision costs a read, never a wrong id — the
|
|
19
|
+
// same discipline as the node table's own `h` index.
|
|
20
|
+
|
|
21
|
+
/** A content canonicalizer: maps a byte span to the canonical representative
|
|
22
|
+
* of its equivalence class. Must be pure and deterministic. Returning the
|
|
23
|
+
* input unchanged is always sound (the class is then {input}). */
|
|
24
|
+
export type Canon = (bytes: Uint8Array) => Uint8Array;
|
|
25
|
+
|
|
26
|
+
const dec = new TextDecoder("utf-8", { fatal: false });
|
|
27
|
+
const enc = new TextEncoder();
|
|
28
|
+
|
|
29
|
+
/** The TEXT canonicalizer: Unicode-aware equivalence over every character
|
|
30
|
+
* variation that does not change what the text SAYS —
|
|
31
|
+
*
|
|
32
|
+
* • compatibility normalization (NFKC): full-width forms, ligatures,
|
|
33
|
+
* composed vs decomposed accents collapse to one representation;
|
|
34
|
+
* • case folding (locale-independent lowercase after NFKC — the standard
|
|
35
|
+
* simple fold);
|
|
36
|
+
* • whitespace: every INTERIOR run of Unicode whitespace becomes one plain
|
|
37
|
+
* space. EDGE whitespace is preserved verbatim: a span's leading or
|
|
38
|
+
* trailing separator belongs BETWEEN forms, not to the form — trimming
|
|
39
|
+
* it would let a recognised span swallow the boundary byte that
|
|
40
|
+
* separates it from its neighbour (observed: "ice fire" composing to
|
|
41
|
+
* "coldhot" because the span "ice " matched the stored "ice").
|
|
42
|
+
*
|
|
43
|
+
* "WHAT IS", "What is" and "what is" share one canonical form. This is
|
|
44
|
+
* deliberately conservative: punctuation, digits and word order are content
|
|
45
|
+
* and pass through untouched. */
|
|
46
|
+
export function textCanon(bytes: Uint8Array): Uint8Array {
|
|
47
|
+
const s = dec
|
|
48
|
+
.decode(bytes)
|
|
49
|
+
.normalize("NFKC")
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/(\S)\s+(?=\S)/g, "$1 ");
|
|
52
|
+
return enc.encode(s);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 32-bit FNV-1a over a canonical key — the integer the store's canon index
|
|
56
|
+
* is keyed on. Same construction as the node table's content hash; a
|
|
57
|
+
* collision is resolved by verifying canon(stored) === key, never trusted. */
|
|
58
|
+
export function canonHash(key: Uint8Array): number {
|
|
59
|
+
let h = 0x811c9dc5 >>> 0;
|
|
60
|
+
for (let i = 0; i < key.length; i++) {
|
|
61
|
+
h ^= key[i];
|
|
62
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
63
|
+
}
|
|
64
|
+
return h >>> 0;
|
|
65
|
+
}
|
package/src/geometry.ts
CHANGED
|
@@ -314,17 +314,32 @@ export function knownPrefixLength(
|
|
|
314
314
|
/** Bytes → Sema tree. `leafAt` and `lookup` are store capabilities for
|
|
315
315
|
* detecting previously-stored prefixes so the river can split at the
|
|
316
316
|
* correct boundary. Pass them through from `perceive`; the geometry
|
|
317
|
-
* computes the stable prefix internally.
|
|
317
|
+
* computes the stable prefix internally.
|
|
318
|
+
*
|
|
319
|
+
* `boundaries` is the CALLER-computed stable-prefix boundary set (§10.3):
|
|
320
|
+
* strictly-increasing proper byte offsets, each the length of a prefix that
|
|
321
|
+
* is already a stored whole-stream form. When given, the fold splits into
|
|
322
|
+
* the segments between consecutive boundaries — each folded independently,
|
|
323
|
+
* exactly as it folded when it was learned — and the segment roots join
|
|
324
|
+
* LEFT-NESTED (((s₀·s₁)·s₂)…), so every learnt cumulative-context root
|
|
325
|
+
* reappears as an identical subtree (and, by hash-consing, the very same
|
|
326
|
+
* node) inside the grown stream. This is what lets a conversation's next
|
|
327
|
+
* turn extend perception instead of refolding it: identical prefixes
|
|
328
|
+
* produce identical subtrees regardless of what follows them. */
|
|
318
329
|
export function bytesToTree(
|
|
319
330
|
space: Space,
|
|
320
331
|
alphabet: Alphabet,
|
|
321
332
|
bytes: Uint8Array,
|
|
322
333
|
leafAt?: (i: number) => number | null,
|
|
323
334
|
lookup?: (leafIds: number[]) => number | null,
|
|
335
|
+
boundaries?: readonly number[],
|
|
324
336
|
): Sema {
|
|
325
337
|
if (bytes.length === 0) {
|
|
326
338
|
return sema(alphabet.vecs[0], new Uint8Array(0), null);
|
|
327
339
|
}
|
|
340
|
+
if (boundaries !== undefined && boundaries.length > 0) {
|
|
341
|
+
return stablePrefixFold(space, alphabet, bytes, boundaries);
|
|
342
|
+
}
|
|
328
343
|
const sb = (leafAt && lookup) ? knownPrefixLength(bytes, leafAt, lookup) : 0;
|
|
329
344
|
return riverFold(
|
|
330
345
|
space,
|
|
@@ -333,6 +348,83 @@ export function bytesToTree(
|
|
|
333
348
|
).tree;
|
|
334
349
|
}
|
|
335
350
|
|
|
351
|
+
/** The stable-prefix segmented fold (§10.3). Each segment between
|
|
352
|
+
* consecutive boundaries folds PLAINLY and independently; segment roots
|
|
353
|
+
* join left-nested, and only the final root is normalized (the linear-fold
|
|
354
|
+
* contract: one normalize per perception). A segment's own inner splits
|
|
355
|
+
* need no recursion here: a nested learnt prefix is itself an earlier
|
|
356
|
+
* boundary, so the left-nested join reproduces every intermediate learnt
|
|
357
|
+
* root ((s₀·s₁) IS the root the store learnt for the first two segments'
|
|
358
|
+
* bytes, and so on). */
|
|
359
|
+
function stablePrefixFold(
|
|
360
|
+
space: Space,
|
|
361
|
+
alphabet: Alphabet,
|
|
362
|
+
bytes: Uint8Array,
|
|
363
|
+
boundaries: readonly number[],
|
|
364
|
+
): Sema {
|
|
365
|
+
const cuts: number[] = [];
|
|
366
|
+
let prev = 0;
|
|
367
|
+
for (const b of boundaries) {
|
|
368
|
+
if (b > prev && b < bytes.length) {
|
|
369
|
+
cuts.push(b);
|
|
370
|
+
prev = b;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (cuts.length === 0) {
|
|
374
|
+
return riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
|
|
375
|
+
}
|
|
376
|
+
const edges = [0, ...cuts, bytes.length];
|
|
377
|
+
const segs: Folded[] = [];
|
|
378
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
379
|
+
const seg = bytes.subarray(edges[i], edges[i + 1]);
|
|
380
|
+
segs.push(riverFoldRaw(space, bytesToLeaves(alphabet, seg)));
|
|
381
|
+
}
|
|
382
|
+
let cur = segs[0];
|
|
383
|
+
for (let i = 1; i < segs.length; i++) cur = fold2(space, cur, segs[i]);
|
|
384
|
+
normalize(cur.tree.v);
|
|
385
|
+
return cur.tree;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Join two folded items as one 2-kid branch — the top-level join of the
|
|
389
|
+
* stable-prefix fold, identical FP ops to foldSlice's seat-bound
|
|
390
|
+
* accumulation over a group of two. Unnormalized (interior). */
|
|
391
|
+
function fold2(space: Space, a: Folded, b: Folded): Folded {
|
|
392
|
+
const D = space.D;
|
|
393
|
+
const gist = new Float32Array(D);
|
|
394
|
+
const kids = [a.tree, b.tree];
|
|
395
|
+
for (let k = 0; k < 2; k++) {
|
|
396
|
+
const seat = space.seats[k].fwd;
|
|
397
|
+
const v = kids[k].v;
|
|
398
|
+
for (let d = 0; d < D; d++) gist[d] += v[seat[d]];
|
|
399
|
+
}
|
|
400
|
+
return { tree: sema(gist, null, kids), len: a.len + b.len };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Plain river fold WITHOUT the final root normalize — the segment-level
|
|
404
|
+
* building block of {@link stablePrefixFold} (interiors must keep their
|
|
405
|
+
* byte-proportional magnitude; only the whole perception's root is ever
|
|
406
|
+
* normalized). */
|
|
407
|
+
function riverFoldRaw(space: Space, row: Folded[]): Folded {
|
|
408
|
+
if (row.length === 0) {
|
|
409
|
+
const z = new Float32Array(space.D);
|
|
410
|
+
return { tree: sema(z, new Uint8Array(0), null), len: 0 };
|
|
411
|
+
}
|
|
412
|
+
if (row.length === 1) return row[0];
|
|
413
|
+
let level = row;
|
|
414
|
+
while (level.length > 1) {
|
|
415
|
+
const next: Folded[] = [];
|
|
416
|
+
foldSlice(space, level, 0, level.length, next, false);
|
|
417
|
+
if (next.length === level.length) {
|
|
418
|
+
const forced: Folded[] = [];
|
|
419
|
+
foldSlice(space, next, 0, next.length, forced, true);
|
|
420
|
+
level = forced;
|
|
421
|
+
} else {
|
|
422
|
+
level = next;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return level[0];
|
|
426
|
+
}
|
|
427
|
+
|
|
336
428
|
// ---- pyramid fold (incremental plain perception) ----
|
|
337
429
|
|
|
338
430
|
/** The PLAIN fold's full level pyramid — every level's item list, bottom
|
|
@@ -368,6 +460,18 @@ export function bytesToTreePyramid(
|
|
|
368
460
|
pyramid: { levels: [], bytes: 0 },
|
|
369
461
|
};
|
|
370
462
|
}
|
|
463
|
+
// ZERO GROWTH: the previous pyramid already covers every byte — its top
|
|
464
|
+
// level holds the finished, normalized root. Refolding here would not
|
|
465
|
+
// only waste the work: when the whole span is one full block, the refold's
|
|
466
|
+
// top item is a REUSED raw interior of `prev`, and the final normalize
|
|
467
|
+
// below would mutate that shared raw block in place, corrupting every
|
|
468
|
+
// later incremental fold built on `prev`.
|
|
469
|
+
if (prev && prev.bytes === bytes.length && prev.levels.length > 0) {
|
|
470
|
+
return {
|
|
471
|
+
tree: prev.levels[prev.levels.length - 1][0].tree,
|
|
472
|
+
pyramid: prev,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
371
475
|
const mg = space.maxGroup;
|
|
372
476
|
const reusable = (L: number): ReadonlyArray<Folded> | null => {
|
|
373
477
|
// prev's TOPMOST level holds its normalized ROOT — reusable blocks must
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./mind/index.js";
|
|
|
12
12
|
export * from "./store-sqlite.js";
|
|
13
13
|
export * from "./config.js";
|
|
14
14
|
export * from "./extension.js";
|
|
15
|
+
export * from "./canon.js";
|
|
15
16
|
export * from "./ingest-cache.js";
|
|
16
17
|
// rabitq-ivf: the partitioned (IVF) vector index over 1-bit RaBitQ codes.
|
|
17
18
|
export {
|
package/src/mind/attention.ts
CHANGED
|
@@ -27,7 +27,7 @@ import type {
|
|
|
27
27
|
SaturationInfo,
|
|
28
28
|
} from "./types.js";
|
|
29
29
|
import { consensusFloor, dominates, estimatorNoise } from "../geometry.js";
|
|
30
|
-
import { foldTree, gistOf, perceive, read } from "./primitives.js";
|
|
30
|
+
import { foldTree, gistOf, latin1Key, perceive, read } from "./primitives.js";
|
|
31
31
|
import { recognise } from "./recognition.js";
|
|
32
32
|
import { leafIdRun } from "./canonical.js";
|
|
33
33
|
import { corpusN, edgeAncestors } from "./traverse.js";
|
|
@@ -65,14 +65,19 @@ export async function climbAttentionAll(
|
|
|
65
65
|
k: number,
|
|
66
66
|
mode: DFMode = "inverse",
|
|
67
67
|
): Promise<AttentionRead> {
|
|
68
|
+
// Content-keyed memo — works for both single-turn respond() and multi-turn
|
|
69
|
+
// respondTurn(). Skipped while tracing.
|
|
68
70
|
if (ctx.climbMemo && !ctx.trace) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const contentKey = latin1Key(query);
|
|
72
|
+
const modeKey = `${k}:${mode}`;
|
|
73
|
+
let byRead = ctx.climbMemo.get(contentKey);
|
|
74
|
+
if (byRead === undefined) {
|
|
75
|
+
ctx.climbMemo.set(contentKey, byRead = new Map());
|
|
76
|
+
}
|
|
77
|
+
const hit = byRead.get(modeKey);
|
|
73
78
|
if (hit !== undefined) return hit;
|
|
74
79
|
const read = await computeAttention(ctx, query, k, mode);
|
|
75
|
-
byRead.set(
|
|
80
|
+
byRead.set(modeKey, read);
|
|
76
81
|
return read;
|
|
77
82
|
}
|
|
78
83
|
return computeAttention(ctx, query, k, mode);
|
package/src/mind/learning.ts
CHANGED
|
@@ -7,7 +7,12 @@ import { Vec } from "../vec.js";
|
|
|
7
7
|
import { bindSeat, companySignature, isChunk, Sema } from "../sema.js";
|
|
8
8
|
import type { Input, MindContext } from "./types.js";
|
|
9
9
|
import { changedNodes } from "./types.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
inputBytes,
|
|
12
|
+
perceive,
|
|
13
|
+
perceiveDeposit,
|
|
14
|
+
resolve,
|
|
15
|
+
} from "./primitives.js";
|
|
11
16
|
import { canonicalWindows, leafIdPrefix } from "./canonical.js";
|
|
12
17
|
import { fold as foldVecs } from "../sema.js";
|
|
13
18
|
|
|
@@ -171,6 +176,38 @@ export async function ingestOne(
|
|
|
171
176
|
return Object.assign(tree, { id: rootId });
|
|
172
177
|
}
|
|
173
178
|
|
|
179
|
+
/** For each right-edge suffix of the context bytes, resolve it against the
|
|
180
|
+
* store. A suffix whose resolved node is already a known form inherits the
|
|
181
|
+
* continuation edge. Gate: ≥ 2 structural parents (reused across deposits),
|
|
182
|
+
* or (halo > 0 ∧ already an edge source). Pure answers do not qualify. */
|
|
183
|
+
async function propagateSuffixes(
|
|
184
|
+
ctx: MindContext,
|
|
185
|
+
src: number,
|
|
186
|
+
dst: number,
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
const W = ctx.space.maxGroup;
|
|
189
|
+
const bytes = ctx.store.bytes(src);
|
|
190
|
+
const n = bytes.length;
|
|
191
|
+
if (n < 2 * W) return;
|
|
192
|
+
// Existence prefilter — the write side of the canonical contract: every
|
|
193
|
+
// deposit interns its WHOLE byte stream as a flat branch of per-byte leaf
|
|
194
|
+
// ids (deposit(), canonical.ts). A suffix is a stored form exactly when
|
|
195
|
+
// that flat twin exists, so one content-hash probe per offset decides;
|
|
196
|
+
// only a hit pays for resolve()'s deposit-shaped perception. This keeps
|
|
197
|
+
// the scan free of river folds — O(1) probes over cheap byte hashes
|
|
198
|
+
// instead of O(suffix) vector folds per offset.
|
|
199
|
+
const leafIds = leafIdPrefix(ctx, bytes);
|
|
200
|
+
for (let i = 1; i <= n - W; i++) {
|
|
201
|
+
if (ctx.store.findBranch(leafIds.slice(i)) === null) continue;
|
|
202
|
+
const id = resolve(ctx, bytes.subarray(i));
|
|
203
|
+
if (id === null || id === src) continue;
|
|
204
|
+
const known = ctx.store.parentsFirst(id, 2).length >= 2 ||
|
|
205
|
+
(ctx.store.haloMass(id) > 0 && ctx.store.hasNext(id));
|
|
206
|
+
if (!known) continue;
|
|
207
|
+
await ctx.store.link(id, dst);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
174
211
|
/** Ingest a pair (context, continuation) — learn an edge and pour halos. */
|
|
175
212
|
export async function ingestPair(
|
|
176
213
|
ctx: MindContext,
|
|
@@ -182,6 +219,7 @@ export async function ingestPair(
|
|
|
182
219
|
const ctxId = c.rootId, contId = cont_.rootId;
|
|
183
220
|
|
|
184
221
|
await ctx.store.link(ctxId, contId);
|
|
222
|
+
await propagateSuffixes(ctx, ctxId, contId);
|
|
185
223
|
|
|
186
224
|
// Halos pour company SIGNATURES (identity), not gists (content) — see
|
|
187
225
|
// companySignature in sema.ts.
|
package/src/mind/match.ts
CHANGED
|
@@ -446,8 +446,10 @@ export async function follow(
|
|
|
446
446
|
* `guide` the context whose gist resonates with the query wins (seat
|
|
447
447
|
* symmetry) — without one, the most-corroborated context wins (poured halo
|
|
448
448
|
* MASS, the direct measure of how many episodes established it), falling
|
|
449
|
-
* back to first-learnt on equal mass.
|
|
450
|
-
*
|
|
449
|
+
* back to first-learnt on equal mass. Among many predecessors RECIPROCAL
|
|
450
|
+
* ones (mutual edges) are preferred when any exist (RC5). Callers that
|
|
451
|
+
* HAVE a query gist must pass it, or they silently change disambiguation
|
|
452
|
+
* regime.
|
|
451
453
|
*
|
|
452
454
|
* `rev`, when the caller has already materialised prevOf (one read per
|
|
453
455
|
* relation — a hub's reverse fan-in is corpus-sized), is reused instead of
|
|
@@ -466,11 +468,32 @@ export function reverseContext(
|
|
|
466
468
|
// keeps the single-predecessor shortcut exact.
|
|
467
469
|
const candidates = rev ?? ctx.store.prevFirst(id, hubBound(ctx));
|
|
468
470
|
if (candidates.length === 0) return null;
|
|
469
|
-
|
|
470
|
-
|
|
471
|
+
// RECIPROCAL PREFERENCE: among many predecessors, one that `id` also
|
|
472
|
+
// continues TO (cand → id AND id → cand both learnt) is a mutually
|
|
473
|
+
// established pairing — the strongest structural evidence a predecessor
|
|
474
|
+
// can carry (bidirectional training deposits both directions of a genuine
|
|
475
|
+
// pair). A bare predecessor is one episode's adjacency; guide-resonance
|
|
476
|
+
// over bare predecessors favours whichever stored document merely
|
|
477
|
+
// CONTAINS the query's bytes (the linear fold's cosine is byte overlap —
|
|
478
|
+
// the observed "merci → unrelated French document" failure). One capped
|
|
479
|
+
// forward read decides; when no reciprocal exists, behaviour is unchanged
|
|
480
|
+
// — bare predecessors ARE the honest answer for a shared deposited
|
|
481
|
+
// continuation (two questions → one answer; audited by 31-audit C1), and
|
|
482
|
+
// this arm serves every mechanism's reverse projection, so abstaining
|
|
483
|
+
// here starves far more than the one containment failure it would fix.
|
|
484
|
+
let pool: readonly number[] = candidates;
|
|
485
|
+
if (candidates.length > 1) {
|
|
486
|
+
const fwd = new Set(ctx.store.nextFirst(id, hubBound(ctx)));
|
|
487
|
+
if (fwd.size > 0) {
|
|
488
|
+
const mutual = candidates.filter((c) => fwd.has(c));
|
|
489
|
+
if (mutual.length > 0) pool = mutual;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const pick = pool.length === 1
|
|
493
|
+
? pool[0]
|
|
471
494
|
: guide
|
|
472
|
-
? chooseAmong(ctx,
|
|
473
|
-
: pickByMass(ctx,
|
|
495
|
+
? chooseAmong(ctx, pool, guide).id
|
|
496
|
+
: pickByMass(ctx, pool);
|
|
474
497
|
const g = read(ctx, pick);
|
|
475
498
|
return g.length > 0 ? g : null;
|
|
476
499
|
}
|
|
@@ -345,9 +345,24 @@ export async function counterfactualTransfer(
|
|
|
345
345
|
// corrupted-store read now falls back here instead of voicing a hollow
|
|
346
346
|
// seat into the comparison — the same "empty bytes are no grounding"
|
|
347
347
|
// invariant project() has always enforced.
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
348
|
+
// A node that only ever CONTINUES — an edge SOURCE with no predecessor —
|
|
349
|
+
// is a learnt CONTEXT (a stored question-shaped frame), not an entity.
|
|
350
|
+
// Voicing it verbatim answers a question with a question (the observed
|
|
351
|
+
// cast echo: two stored questions concatenated as an "answer"). The
|
|
352
|
+
// context's role is established by what it LEADS TO, so its seat is its
|
|
353
|
+
// own continuation — the fact — with the reverse projection and the raw
|
|
354
|
+
// bytes as the ordinary fallbacks for genuine entities.
|
|
355
|
+
const seatOfNode = async (
|
|
356
|
+
id: number,
|
|
357
|
+
fallback: Uint8Array,
|
|
358
|
+
): Promise<Uint8Array> => {
|
|
359
|
+
if (ctx.store.prevCount(id) === 0 && ctx.store.hasNext(id)) {
|
|
360
|
+
const fwd = await follow(ctx, id, qv);
|
|
361
|
+
if (fwd !== null) return fwd;
|
|
362
|
+
}
|
|
363
|
+
return reverseContext(ctx, id, qv) ?? fallback;
|
|
364
|
+
};
|
|
365
|
+
const seatOf = (p: Point): Promise<Uint8Array> => seatOfNode(p.anchor, p.ctx);
|
|
351
366
|
interface AnalogCandidate {
|
|
352
367
|
anchor: number;
|
|
353
368
|
/** The point this candidate came from, or null when it is a nextOf
|
|
@@ -494,10 +509,10 @@ export async function counterfactualTransfer(
|
|
|
494
509
|
[],
|
|
495
510
|
"the two structures keep distributional company beyond chance — genuine analogs",
|
|
496
511
|
);
|
|
497
|
-
const a = seatOf(dominant);
|
|
512
|
+
const a = await seatOf(dominant);
|
|
498
513
|
const b = bestAnalog.point !== null
|
|
499
|
-
? seatOf(bestAnalog.point)
|
|
500
|
-
: seatOfNode(bestAnalog.anchor, read(ctx, bestAnalog.anchor));
|
|
514
|
+
? await seatOf(bestAnalog.point)
|
|
515
|
+
: await seatOfNode(bestAnalog.anchor, read(ctx, bestAnalog.anchor));
|
|
501
516
|
const answer = await joinWithBridge(ctx, a, b);
|
|
502
517
|
record(
|
|
503
518
|
answer,
|
|
@@ -116,6 +116,20 @@ export async function confluenceJoin(
|
|
|
116
116
|
* recognised common form). */
|
|
117
117
|
held: Array<[number, number]>;
|
|
118
118
|
}
|
|
119
|
+
// A constraint must bind a CONSTITUENT, not a shard. A genuinely shared
|
|
120
|
+
// form weaves a contiguous RUN of shared discriminative windows — its
|
|
121
|
+
// merged cover span is the form's own length, beyond one perception
|
|
122
|
+
// quantum (2W: the same "two quanta of structure" bar the conjunctive
|
|
123
|
+
// precondition `query.length < 2W` and CAST's weave live under). A long
|
|
124
|
+
// stored document holds thousands of W-windows and will hold a FEW of any
|
|
125
|
+
// query's by accident, but accidental sharing is one window (a merged
|
|
126
|
+
// span of W, at most ~W+overlap bytes: "ow ma", "ys a", "ías " —
|
|
127
|
+
// observed), never a run (" translucent", " featherlight" — the genuine
|
|
128
|
+
// constraints). Shard-bound streams are no constraints, and their meets
|
|
129
|
+
// are connective debris (". Sure,", "ngul" — observed).
|
|
130
|
+
const bindsAConstituent = (cover: Array<[number, number]>): boolean =>
|
|
131
|
+
cover.some(([cs, ce]) => ce - cs >= 2 * W);
|
|
132
|
+
|
|
119
133
|
const streams: Stream[] = [];
|
|
120
134
|
const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
|
|
121
135
|
for (const cand of rankedCapped) {
|
|
@@ -134,7 +148,7 @@ export async function confluenceJoin(
|
|
|
134
148
|
if (curC !== null && off <= curC[1]) curC[1] = off + W;
|
|
135
149
|
else cover.push(curC = [off, off + W]);
|
|
136
150
|
}
|
|
137
|
-
if (cover.length > 0) {
|
|
151
|
+
if (cover.length > 0 && bindsAConstituent(cover)) {
|
|
138
152
|
streams.push({ anchor: cand.anchor, vote: cand.vote, ids, cover, held });
|
|
139
153
|
}
|
|
140
154
|
}
|