@hviana/sema 0.1.0

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.
Files changed (110) hide show
  1. package/AGENTS.md +470 -0
  2. package/AUTHORS.md +12 -0
  3. package/COMMERCIAL-LICENSE.md +19 -0
  4. package/CONTRIBUTING.md +15 -0
  5. package/HOW_IT_WORKS.md +4210 -0
  6. package/LICENSE.md +117 -0
  7. package/README.md +290 -0
  8. package/TRADEMARKS.md +19 -0
  9. package/example/demo.ts +46 -0
  10. package/example/train_base.ts +2675 -0
  11. package/index.html +893 -0
  12. package/package.json +25 -0
  13. package/src/alphabet.ts +34 -0
  14. package/src/alu/README.md +332 -0
  15. package/src/alu/src/alu.ts +541 -0
  16. package/src/alu/src/expr.ts +339 -0
  17. package/src/alu/src/index.ts +115 -0
  18. package/src/alu/src/kernel-arith.ts +377 -0
  19. package/src/alu/src/kernel-bits.ts +199 -0
  20. package/src/alu/src/kernel-logic.ts +102 -0
  21. package/src/alu/src/kernel-nd.ts +235 -0
  22. package/src/alu/src/kernel-numeric.ts +466 -0
  23. package/src/alu/src/operation.ts +344 -0
  24. package/src/alu/src/parser.ts +574 -0
  25. package/src/alu/src/resonance.ts +161 -0
  26. package/src/alu/src/text.ts +83 -0
  27. package/src/alu/src/value.ts +327 -0
  28. package/src/alu/test/alu.test.ts +1004 -0
  29. package/src/bytes.ts +62 -0
  30. package/src/config.ts +227 -0
  31. package/src/derive/README.md +295 -0
  32. package/src/derive/src/deduction.ts +263 -0
  33. package/src/derive/src/index.ts +24 -0
  34. package/src/derive/src/priority-queue.ts +70 -0
  35. package/src/derive/src/rewrite.ts +127 -0
  36. package/src/derive/src/trie.ts +242 -0
  37. package/src/derive/test/derive.test.ts +137 -0
  38. package/src/extension.ts +42 -0
  39. package/src/geometry.ts +511 -0
  40. package/src/index.ts +38 -0
  41. package/src/ingest-cache.ts +224 -0
  42. package/src/mind/articulation.ts +137 -0
  43. package/src/mind/attention.ts +1061 -0
  44. package/src/mind/canonical.ts +107 -0
  45. package/src/mind/graph-search.ts +1100 -0
  46. package/src/mind/index.ts +18 -0
  47. package/src/mind/junction.ts +347 -0
  48. package/src/mind/learning.ts +246 -0
  49. package/src/mind/match.ts +507 -0
  50. package/src/mind/mechanisms/alu.ts +33 -0
  51. package/src/mind/mechanisms/cast.ts +568 -0
  52. package/src/mind/mechanisms/confluence.ts +268 -0
  53. package/src/mind/mechanisms/cover.ts +248 -0
  54. package/src/mind/mechanisms/extraction.ts +422 -0
  55. package/src/mind/mechanisms/recall.ts +241 -0
  56. package/src/mind/mind.ts +483 -0
  57. package/src/mind/pipeline-mechanism.ts +326 -0
  58. package/src/mind/pipeline.ts +300 -0
  59. package/src/mind/primitives.ts +201 -0
  60. package/src/mind/rationale.ts +275 -0
  61. package/src/mind/reasoning.ts +198 -0
  62. package/src/mind/recognition.ts +234 -0
  63. package/src/mind/resonance.ts +0 -0
  64. package/src/mind/trace.ts +108 -0
  65. package/src/mind/traverse.ts +556 -0
  66. package/src/mind/types.ts +281 -0
  67. package/src/rabitq-hnsw/README.md +303 -0
  68. package/src/rabitq-hnsw/src/database.ts +492 -0
  69. package/src/rabitq-hnsw/src/heap.ts +90 -0
  70. package/src/rabitq-hnsw/src/hnsw.ts +514 -0
  71. package/src/rabitq-hnsw/src/index.ts +15 -0
  72. package/src/rabitq-hnsw/src/prng.ts +39 -0
  73. package/src/rabitq-hnsw/src/rabitq.ts +308 -0
  74. package/src/rabitq-hnsw/src/store.ts +994 -0
  75. package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
  76. package/src/store-sqlite.ts +928 -0
  77. package/src/store.ts +2148 -0
  78. package/src/vec.ts +124 -0
  79. package/test/00-extract.test.mjs +151 -0
  80. package/test/01-floor.test.mjs +20 -0
  81. package/test/02-roundtrip.test.mjs +83 -0
  82. package/test/03-recall.test.mjs +98 -0
  83. package/test/04-think.test.mjs +627 -0
  84. package/test/05-concepts.test.mjs +84 -0
  85. package/test/08-storage.test.mjs +948 -0
  86. package/test/09-edges.test.mjs +65 -0
  87. package/test/11-universality.test.mjs +180 -0
  88. package/test/13-conversation.test.mjs +228 -0
  89. package/test/14-scaling.test.mjs +503 -0
  90. package/test/15-decomposition-gap.test.mjs +0 -0
  91. package/test/16-bridge.test.mjs +250 -0
  92. package/test/17-intelligence.test.mjs +240 -0
  93. package/test/18-alu.test.mjs +209 -0
  94. package/test/19-nd.test.mjs +254 -0
  95. package/test/20-stability.test.mjs +489 -0
  96. package/test/21-partial.test.mjs +158 -0
  97. package/test/22-multihop.test.mjs +130 -0
  98. package/test/23-rationale.test.mjs +316 -0
  99. package/test/24-generalization.test.mjs +416 -0
  100. package/test/25-embedding.test.mjs +75 -0
  101. package/test/26-cooperative.test.mjs +89 -0
  102. package/test/27-saturation-drop.test.mjs +637 -0
  103. package/test/28-unknowable.test.mjs +88 -0
  104. package/test/29-counterfactual.test.mjs +476 -0
  105. package/test/30-conflict-resolution.test.mjs +166 -0
  106. package/test/31-audit.test.mjs +288 -0
  107. package/test/32-confluence.test.mjs +173 -0
  108. package/test/33-multi-candidate.test.mjs +222 -0
  109. package/test/34-cross-region.test.mjs +252 -0
  110. package/tsconfig.json +16 -0
@@ -0,0 +1,224 @@
1
+ // ingest-cache.ts — ingest-level cache for Mind.
2
+ //
3
+ // Wraps a Mind and memoises the expensive, content-determined half of ingestion:
4
+ // turning ONE input (a context, a continuation, or a bare experience) into its
5
+ // interned graph nodes. Perception (riverFold) and interning are a PURE function
6
+ // of the input bytes — the same bytes always cut the same way and hash-cons to
7
+ // the same node ids — so the result can be cached by content and reused wherever
8
+ // those same bytes appear again, in ANY role.
9
+ //
10
+ // This is why the cache keys on the single input, not on the (context,
11
+ // continuation) pair: real training corpora repeat content at the INPUT level far
12
+ // more than at the pair level — a shared system/tool preamble in front of every
13
+ // row, a common answer ("Yes.", a refusal) under many questions, an accumulated
14
+ // dialogue prefix, an exactly-duplicated row. A pair-level key sees none of these
15
+ // (the pair as a whole is almost always unique); the input-level key catches them
16
+ // all, and a context that recurs as a continuation (or vice versa) hits too.
17
+ //
18
+ // On a cache hit only the cheap relational writes are replayed — the edge and the
19
+ // two halo pours for a pair, or the part-chain links for a single — while the
20
+ // perceive + internTree work (the dominant cost, and on a large store the dominant
21
+ // VECTOR-INDEX cost too) is skipped entirely. The interned node ids are stable
22
+ // across eviction: a re-perceive of evicted content reproduces the same ids by
23
+ // the store's hash-cons, so correctness never depends on a hit.
24
+ //
25
+ // DEPOSITION IS THE MIND'S OWN: every miss goes through the same
26
+ // {@link deposit} the direct Mind.ingest path uses — perception, interning,
27
+ // sub-span/window indexing, durable CONTAINMENT edges, whole-stream flat
28
+ // branch, changed-node tracking against the Mind's `_prevSeen` — so a store
29
+ // trained through this cache is structurally IDENTICAL to one trained by
30
+ // calling mind.ingest() directly (this class used to re-implement the deposit
31
+ // and drifted: no indexSubSpans/addContainer, wrong part-link stride). Only
32
+ // the memo layer lives here. Behaviour is identical to mind.ingest() (links
33
+ // and halos are reinforced on every call), just faster when content repeats.
34
+
35
+ import { Mind } from "./mind/index.js";
36
+ import {
37
+ deposit,
38
+ dispatchIngest,
39
+ ingestOne as depositOne,
40
+ } from "./mind/learning.js";
41
+ import { bindSeat, companySignature, type Sema } from "./sema.js";
42
+ import type { Input } from "./mind/index.js";
43
+ import { BoundedMap } from "./store.js";
44
+ import type { Vec } from "./vec.js";
45
+
46
+ /** The interned result of perceiving + interning ONE input.
47
+ *
48
+ * Only the root vector and the node ids are kept — the full Sema tree (hundreds
49
+ * of nodes) is GC'd. `rootV` is needed for the halo binds (a pair seats each
50
+ * side's gist into the other's halo) and for the Sema returned by a single
51
+ * ingest; `partIds` are the root's immediate children, the chain a single
52
+ * experience links together (`[rootId]` when the root is itself a leaf). */
53
+ interface Interned {
54
+ rootV: Vec;
55
+ rootId: number;
56
+ partIds: number[];
57
+ /** Byte cost of the memo KEY this entry is stored under. The key of a
58
+ * string input is the whole input ("S:" + text), so on long continuations
59
+ * it dwarfs the value — charging it to the LRU budget is what keeps the
60
+ * memo's real memory at ingestCacheBytes instead of a multiple of it. */
61
+ keyBytes: number;
62
+ }
63
+
64
+ /**
65
+ * An ingest cache layered over a Mind.
66
+ *
67
+ * Usage (drop-in for mind.ingest in training loops):
68
+ * const ci = new CachedIngest(mind);
69
+ * await ci.ingest(ctx, cont); // caches both sides
70
+ * await ci.ingest("some text"); // caches the single text
71
+ * await ci.ingest([[ctx, cont]]); // array form
72
+ *
73
+ * All other Mind methods (respond, save, …) are accessed on `ci.mind`.
74
+ */
75
+ export class CachedIngest {
76
+ readonly mind: Mind;
77
+
78
+ /** One unified, content-addressed memo of interned inputs. Each value holds
79
+ * Float32 root vector (D·4 bytes) plus a small id array, so the byte budget
80
+ * is spent on the inputs most worth remembering (LRU). */
81
+ private _memo!: BoundedMap<string, Interned>;
82
+
83
+ hits = 0;
84
+ misses = 0;
85
+
86
+ constructor(mind: Mind) {
87
+ this.mind = mind;
88
+ this._memo = this.newMemo();
89
+ }
90
+
91
+ private newMemo(): BoundedMap<string, Interned> {
92
+ return new BoundedMap<string, Interned>(
93
+ this.mind.cfg.store.ingestCacheBytes,
94
+ (c) => c.rootV.byteLength + c.partIds.length * 4 + 8 + c.keyBytes,
95
+ );
96
+ }
97
+
98
+ // ── cache key ───────────────────────────────────────────────────────────
99
+ // Content key for an input, or null when the input cannot be keyed reliably
100
+ // — in which case it BYPASSES the cache (recomputed every time) rather than
101
+ // risk a collision returning the wrong nodes. A string is its own key; raw
102
+ // bytes hash by FNV-1a (+ length, so a hash collision still needs an equal
103
+ // length to alias); a Grid / Grid[] has no cheap stable key, so it bypasses.
104
+
105
+ private keyOf(input: Input): string | null {
106
+ if (typeof input === "string") return "S:" + input;
107
+ if (input instanceof Uint8Array) {
108
+ // FNV-1a — no spread onto the call stack, safe on large binary inputs.
109
+ let h = 0x811c9dc5 >>> 0;
110
+ for (let i = 0; i < input.length; i++) {
111
+ h ^= input[i];
112
+ h = Math.imul(h, 0x01000193) >>> 0;
113
+ }
114
+ return "B:" + (h >>> 0).toString(16) + ":" + input.length;
115
+ }
116
+ return null; // Grid / Grid[] — bypass the cache
117
+ }
118
+
119
+ // ── public API ────────────────────────────────────────────────────────
120
+
121
+ async ingest(
122
+ input: Input | (Input | [Input, Input])[],
123
+ second?: Input,
124
+ ): Promise<(Sema & { id: number }) | undefined> {
125
+ // One shape-reading for both ingest paths — see {@link dispatchIngest}.
126
+ return dispatchIngest(
127
+ input,
128
+ second,
129
+ (i) => this.ingestOne(i),
130
+ (a, b) => this.ingestPair(a, b),
131
+ );
132
+ }
133
+
134
+ // ── the one cached step: perceive + intern + index ONE input ──────────────
135
+
136
+ /** Resolve an input to its interned nodes, from the memo when its content has
137
+ * been seen before, else by the Mind's OWN untracked {@link deposit}
138
+ * (perceive + intern + sub-span/containment indexing — identical to the
139
+ * direct path; those writes are durable and idempotent, so a later memo hit
140
+ * legitimately skips them). This is the single expensive operation both
141
+ * ingest paths share; caching it here is what makes every repeated input —
142
+ * in any role — cheap. */
143
+ private async resolveInput(input: Input): Promise<Interned> {
144
+ const key = this.keyOf(input);
145
+ if (key !== null) {
146
+ const cached = this._memo.get(key);
147
+ if (cached) {
148
+ this.hits++;
149
+ return cached;
150
+ }
151
+ }
152
+ this.misses++;
153
+
154
+ // The full deposit, untracked (a continuation must not break the tracked
155
+ // deposit chain). `partIds` are the root's immediate children, already
156
+ // interned by the deposit — free to capture. Only the root vector and
157
+ // ids are retained; the tree is GC'd.
158
+ const { tree, rootId, ids } = await deposit(this.mind, input, false);
159
+ const partIds: number[] = tree.kids
160
+ ? tree.kids.map((k) => ids.get(k)!)
161
+ : [rootId];
162
+
163
+ const entry: Interned = {
164
+ rootV: tree.v,
165
+ rootId,
166
+ partIds,
167
+ // ~2 bytes per UTF-16 code unit — the key is retained by the map.
168
+ keyBytes: key !== null ? key.length * 2 : 0,
169
+ };
170
+ if (key !== null) this._memo.set(key, entry);
171
+ return entry;
172
+ }
173
+
174
+ // ── pair ingest ───────────────────────────────────────────────────────
175
+
176
+ private async ingestPair(ctx: Input, cont: Input): Promise<void> {
177
+ // The CONTEXT side is TRACKED (it continues the Mind's deposit chain and
178
+ // is the growing, unique-per-turn side — a cache miss anyway), via the
179
+ // same {@link deposit} the direct path uses; the continuation resolves
180
+ // through the memo.
181
+ const c = await deposit(this.mind, ctx, true);
182
+ const b = await this.resolveInput(cont);
183
+
184
+ // EDGE on the FULL context (disambiguates a shared pivot). HALO only on the
185
+ // CHANGED NODES — the coherent new subtrees this turn adds — so a recurring
186
+ // shared prefix never collects halo mass. (See Mind.ingestPair.)
187
+ await this.mind.store.link(c.rootId, b.rootId);
188
+ // Halos pour company SIGNATURES (identity), not gists (content) — see
189
+ // companySignature in sema.ts.
190
+ const contSeat = bindSeat(
191
+ this.mind.space,
192
+ companySignature(this.mind.space, b.rootId),
193
+ 1,
194
+ );
195
+ for (const part of c.changed) {
196
+ const partId = c.ids.get(part)!;
197
+ await this.mind.store.pourHalo(partId, contSeat);
198
+ await this.mind.store.pourHalo(
199
+ b.rootId,
200
+ bindSeat(this.mind.space, companySignature(this.mind.space, partId), 0),
201
+ );
202
+ }
203
+ }
204
+
205
+ // ── one-side ingest ───────────────────────────────────────────────────
206
+
207
+ private async ingestOne(input: Input): Promise<Sema & { id: number }> {
208
+ // A bare experience is the direct path verbatim — tracked deposit, root
209
+ // marked a resonance target, part chain linked at the maxGroup stride.
210
+ // (The tracked side is a memo miss by design, so there is nothing for the
211
+ // cache to add here.)
212
+ return depositOne(this.mind, input);
213
+ }
214
+
215
+ // ── helpers ───────────────────────────────────────────────────────────
216
+
217
+ clear(): void {
218
+ this._memo = this.newMemo();
219
+ }
220
+
221
+ get size(): number {
222
+ return this._memo.size;
223
+ }
224
+ }
@@ -0,0 +1,137 @@
1
+ // articulation.ts — re-voice an answer in the asker's wording (Section 5).
2
+ //
3
+ // articulate — substitute answer forms with the asker's synonyms,
4
+ // using concept (halo) resonance to match the voices.
5
+
6
+ import { Vec } from "../vec.js";
7
+ import type { MindContext } from "./types.js";
8
+ import { spliceAll } from "./types.js";
9
+ import { recognise } from "./recognition.js";
10
+ import { contains } from "./traverse.js";
11
+ import { bestHaloMate } from "./match.js";
12
+ import type { Site } from "./graph-search.js";
13
+ import type { CandidateSpan } from "../derive/src/index.js";
14
+ import { coverSequence } from "../derive/src/index.js";
15
+ import { rItem, rNode, traceDerivation } from "./trace.js";
16
+
17
+ /** Re-voice an answer in the asker's own words. For each recognised form
18
+ * in the answer, find a concept-sibling in the query (by halo resonance)
19
+ * and substitute the asker's wording. The search's own cover mechanism
20
+ * splices the substitutes into the answer exactly where the forms sit. */
21
+ export async function articulate(
22
+ ctx: MindContext,
23
+ answer: Uint8Array,
24
+ query: Uint8Array | null,
25
+ ): Promise<Uint8Array> {
26
+ if (!query || query.length < 2 || answer.length < 2) return answer;
27
+ const store = ctx.store;
28
+ const t = ctx.trace?.enter("articulate", [
29
+ rItem(answer, "answer"),
30
+ rItem(query, "query"),
31
+ ]);
32
+ const keep = (note: string): Uint8Array => {
33
+ t?.done([rItem(answer, "answer")], note);
34
+ return answer;
35
+ };
36
+
37
+ const qCandidates: Array<
38
+ CandidateSpan<{ halo: Vec; bytes: Uint8Array; node: number }>
39
+ > = [];
40
+ for (const s of recognise(ctx, query).sites) {
41
+ // payload < 0 is a SINGLE-BYTE leaf (byte leaves occupy the implicit
42
+ // negative id range −256…−1 — see store.ts). A one-byte form's halo is
43
+ // promiscuous (it keeps company with everything it ever neighboured), so
44
+ // admitting it as a voice or a revoicing target lets e.g. the digit "4"
45
+ // be "re-voiced" into an unrelated digit the query happens to contain.
46
+ // Articulation therefore operates on multi-byte forms only.
47
+ if (s.payload < 0) continue;
48
+ const h = store.halo(s.payload);
49
+ if (!h) continue;
50
+ qCandidates.push({
51
+ start: s.start,
52
+ end: s.end,
53
+ payload: {
54
+ halo: h,
55
+ bytes: query.slice(s.start, s.end),
56
+ node: s.payload,
57
+ },
58
+ });
59
+ }
60
+ if (qCandidates.length === 0) {
61
+ return keep("no asker concept to revoice — answer unchanged");
62
+ }
63
+ if (coverSequence(query.length, qCandidates).spans.length === 0) {
64
+ return keep("no asker concept to revoice — answer unchanged");
65
+ }
66
+ const voices = qCandidates.map((s) => s.payload);
67
+
68
+ const ans = recognise(ctx, answer);
69
+ const substitutions = new Map<number, Uint8Array>();
70
+ for (const s of ans.sites) {
71
+ // Same single-byte-leaf exclusion as the query loop above.
72
+ if (s.payload < 0) continue;
73
+ const h = store.halo(s.payload);
74
+ if (!h) continue;
75
+ const found = bestHaloMate(ctx, h, voices, (v) => v.halo);
76
+ if (!found) continue;
77
+ const voice = found.item;
78
+ if (voice.node === s.payload || contains(ctx, voice.node, s.payload)) {
79
+ continue;
80
+ }
81
+ substitutions.set(s.payload, voice.bytes);
82
+ }
83
+ if (substitutions.size === 0) {
84
+ return keep("no answer form is a synonym of an asker concept");
85
+ }
86
+ ctx.trace?.step(
87
+ "substitute",
88
+ [...substitutions.keys()].map((n) => rNode(ctx, n, "answer-form")),
89
+ [...substitutions.values()].map((b) => rItem(b, "asker-voice")),
90
+ `revoice ${substitutions.size} answer form(s) in the asker's own words (synonym splice)`,
91
+ );
92
+
93
+ const voicedSites = ans.sites.filter((s) => substitutions.has(s.payload));
94
+ const tArtCover = ctx.trace?.enter("cover", [
95
+ ...voicedSites.map((s) =>
96
+ rItem(answer.subarray(s.start, s.end), "form", s.payload, [
97
+ s.start,
98
+ s.end,
99
+ ])
100
+ ),
101
+ ]);
102
+ const solved = ctx.search.cover(
103
+ answer.length,
104
+ voicedSites,
105
+ new Map(),
106
+ ans.leaves,
107
+ ans.splits,
108
+ substitutions,
109
+ undefined,
110
+ undefined,
111
+ ctx.trace ? (steps) => traceDerivation(ctx, steps) : undefined,
112
+ );
113
+ const segs = solved && solved.segs;
114
+ tArtCover?.done(
115
+ segs === null
116
+ ? []
117
+ : segs.map((s) =>
118
+ rItem(s.bytes, s.rec ? "voiced" : "kept", s.node, [s.i, s.j])
119
+ ),
120
+ segs === null
121
+ ? "the revoiced cover did not compose"
122
+ : "lightest derivation of the revoiced answer",
123
+ );
124
+ const result = segs && spliceAll(segs);
125
+ if (result) {
126
+ t?.done(
127
+ [rItem(
128
+ result,
129
+ "voiced",
130
+ ctx.store.findLeaf(result.subarray(0, 1)) ?? undefined,
131
+ )],
132
+ "splice the asker's wording into the answer where the same concept appears",
133
+ );
134
+ return result;
135
+ }
136
+ return keep("the revoiced cover did not compose — answer unchanged");
137
+ }