@hviana/sema 0.2.6 → 0.2.7

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 (131) hide show
  1. package/dist/example/demo.d.ts +1 -0
  2. package/dist/example/demo.js +39 -0
  3. package/dist/example/train_base.d.ts +87 -0
  4. package/dist/example/train_base.js +2252 -0
  5. package/dist/src/alphabet.d.ts +7 -0
  6. package/dist/src/alphabet.js +33 -0
  7. package/dist/src/alu/src/alu.d.ts +185 -0
  8. package/dist/src/alu/src/alu.js +440 -0
  9. package/dist/src/alu/src/expr.d.ts +61 -0
  10. package/dist/src/alu/src/expr.js +318 -0
  11. package/dist/src/alu/src/index.d.ts +11 -0
  12. package/dist/src/alu/src/index.js +19 -0
  13. package/dist/src/alu/src/kernel-arith.d.ts +16 -0
  14. package/dist/src/alu/src/kernel-arith.js +264 -0
  15. package/dist/src/alu/src/kernel-bits.d.ts +19 -0
  16. package/dist/src/alu/src/kernel-bits.js +152 -0
  17. package/dist/src/alu/src/kernel-logic.d.ts +4 -0
  18. package/dist/src/alu/src/kernel-logic.js +60 -0
  19. package/dist/src/alu/src/kernel-nd.d.ts +3 -0
  20. package/dist/src/alu/src/kernel-nd.js +208 -0
  21. package/dist/src/alu/src/kernel-numeric.d.ts +54 -0
  22. package/dist/src/alu/src/kernel-numeric.js +366 -0
  23. package/dist/src/alu/src/operation.d.ts +168 -0
  24. package/dist/src/alu/src/operation.js +189 -0
  25. package/dist/src/alu/src/parser.d.ts +221 -0
  26. package/dist/src/alu/src/parser.js +577 -0
  27. package/dist/src/alu/src/resonance.d.ts +55 -0
  28. package/dist/src/alu/src/resonance.js +126 -0
  29. package/dist/src/alu/src/text.d.ts +31 -0
  30. package/dist/src/alu/src/text.js +73 -0
  31. package/dist/src/alu/src/value.d.ts +109 -0
  32. package/dist/src/alu/src/value.js +300 -0
  33. package/dist/src/alu/test/alu.test.d.ts +1 -0
  34. package/dist/src/alu/test/alu.test.js +764 -0
  35. package/dist/src/bytes.d.ts +14 -0
  36. package/dist/src/bytes.js +59 -0
  37. package/dist/src/canon.d.ts +26 -0
  38. package/dist/src/canon.js +57 -0
  39. package/dist/src/config.d.ts +111 -0
  40. package/dist/src/config.js +91 -0
  41. package/dist/src/derive/src/deduction.d.ts +125 -0
  42. package/dist/src/derive/src/deduction.js +155 -0
  43. package/dist/src/derive/src/index.d.ts +7 -0
  44. package/dist/src/derive/src/index.js +11 -0
  45. package/dist/src/derive/src/priority-queue.d.ts +20 -0
  46. package/dist/src/derive/src/priority-queue.js +73 -0
  47. package/dist/src/derive/src/rewrite.d.ts +56 -0
  48. package/dist/src/derive/src/rewrite.js +100 -0
  49. package/dist/src/derive/src/trie.d.ts +90 -0
  50. package/dist/src/derive/src/trie.js +217 -0
  51. package/dist/src/derive/test/derive.test.d.ts +1 -0
  52. package/dist/src/derive/test/derive.test.js +122 -0
  53. package/dist/src/extension.d.ts +37 -0
  54. package/dist/src/extension.js +7 -0
  55. package/dist/src/geometry.d.ts +202 -0
  56. package/dist/src/geometry.js +575 -0
  57. package/dist/src/index.d.ts +16 -0
  58. package/dist/src/index.js +18 -0
  59. package/dist/src/ingest-cache.d.ts +41 -0
  60. package/dist/src/ingest-cache.js +161 -0
  61. package/dist/src/mind/articulation.d.ts +6 -0
  62. package/dist/src/mind/articulation.js +99 -0
  63. package/dist/src/mind/attention.d.ts +400 -0
  64. package/dist/src/mind/attention.js +1763 -0
  65. package/dist/src/mind/bridge.d.ts +30 -0
  66. package/dist/src/mind/bridge.js +569 -0
  67. package/dist/src/mind/canonical.d.ts +29 -0
  68. package/dist/src/mind/canonical.js +88 -0
  69. package/dist/src/mind/graph-search.d.ts +285 -0
  70. package/dist/src/mind/graph-search.js +956 -0
  71. package/dist/src/mind/index.d.ts +7 -0
  72. package/dist/src/mind/index.js +5 -0
  73. package/dist/src/mind/junction.d.ts +124 -0
  74. package/dist/src/mind/junction.js +317 -0
  75. package/dist/src/mind/learning.d.ts +47 -0
  76. package/dist/src/mind/learning.js +248 -0
  77. package/dist/src/mind/match.d.ts +126 -0
  78. package/dist/src/mind/match.js +441 -0
  79. package/dist/src/mind/mechanisms/alu.d.ts +4 -0
  80. package/dist/src/mind/mechanisms/alu.js +36 -0
  81. package/dist/src/mind/mechanisms/cast.d.ts +89 -0
  82. package/dist/src/mind/mechanisms/cast.js +716 -0
  83. package/dist/src/mind/mechanisms/confluence.d.ts +24 -0
  84. package/dist/src/mind/mechanisms/confluence.js +225 -0
  85. package/dist/src/mind/mechanisms/cover.d.ts +6 -0
  86. package/dist/src/mind/mechanisms/cover.js +203 -0
  87. package/dist/src/mind/mechanisms/extraction.d.ts +67 -0
  88. package/dist/src/mind/mechanisms/extraction.js +387 -0
  89. package/dist/src/mind/mechanisms/recall.d.ts +13 -0
  90. package/dist/src/mind/mechanisms/recall.js +325 -0
  91. package/dist/src/mind/mind.d.ts +276 -0
  92. package/dist/src/mind/mind.js +572 -0
  93. package/dist/src/mind/pipeline-mechanism.d.ts +142 -0
  94. package/dist/src/mind/pipeline-mechanism.js +213 -0
  95. package/dist/src/mind/pipeline.d.ts +20 -0
  96. package/dist/src/mind/pipeline.js +217 -0
  97. package/dist/src/mind/primitives.d.ts +70 -0
  98. package/dist/src/mind/primitives.js +287 -0
  99. package/dist/src/mind/rationale.d.ts +139 -0
  100. package/dist/src/mind/rationale.js +163 -0
  101. package/dist/src/mind/reasoning.d.ts +34 -0
  102. package/dist/src/mind/reasoning.js +240 -0
  103. package/dist/src/mind/recognition.d.ts +20 -0
  104. package/dist/src/mind/recognition.js +390 -0
  105. package/dist/src/mind/resonance.d.ts +23 -0
  106. package/dist/src/mind/resonance.js +0 -0
  107. package/dist/src/mind/trace.d.ts +15 -0
  108. package/dist/src/mind/trace.js +73 -0
  109. package/dist/src/mind/traverse.d.ts +113 -0
  110. package/dist/src/mind/traverse.js +568 -0
  111. package/dist/src/mind/types.d.ts +289 -0
  112. package/dist/src/mind/types.js +130 -0
  113. package/dist/src/rabitq-ivf/src/database.d.ts +113 -0
  114. package/dist/src/rabitq-ivf/src/database.js +201 -0
  115. package/dist/src/rabitq-ivf/src/index.d.ts +7 -0
  116. package/dist/src/rabitq-ivf/src/index.js +4 -0
  117. package/dist/src/rabitq-ivf/src/ivf.d.ts +200 -0
  118. package/dist/src/rabitq-ivf/src/ivf.js +1165 -0
  119. package/dist/src/rabitq-ivf/src/prng.d.ts +19 -0
  120. package/dist/src/rabitq-ivf/src/prng.js +36 -0
  121. package/dist/src/rabitq-ivf/src/rabitq.d.ts +95 -0
  122. package/dist/src/rabitq-ivf/src/rabitq.js +283 -0
  123. package/dist/src/sema.d.ts +31 -0
  124. package/dist/src/sema.js +63 -0
  125. package/dist/src/store-sqlite.d.ts +184 -0
  126. package/dist/src/store-sqlite.js +942 -0
  127. package/dist/src/store.d.ts +663 -0
  128. package/dist/src/store.js +1618 -0
  129. package/dist/src/vec.d.ts +31 -0
  130. package/dist/src/vec.js +109 -0
  131. package/package.json +1 -1
@@ -0,0 +1,287 @@
1
+ // primitives.ts — Address + Read primitives (Section 1 of the mind).
2
+ //
3
+ // Address — bytes → node (perceive, foldTree, resolve)
4
+ // Read — node → bytes (read)
5
+ import { bytesToTree, bytesToTreePyramid, gridToTree, hilbertBytes, stablePrefixFoldIncremental, stackGrids, } from "../geometry.js";
6
+ import { canonHash } from "../canon.js";
7
+ import { bytesEqual } from "../bytes.js";
8
+ import { ALL } from "./types.js";
9
+ // ── Address: bytes → node ──────────────────────────────────────────────
10
+ /** The content key of a byte span — one latin1 char per byte, an exact,
11
+ * collision-free encoding. Spans on the perception path are query-scale
12
+ * (windows, regions, candidate spans), so key construction is far cheaper
13
+ * than the river fold it deduplicates. */
14
+ export function latin1Key(bytes) {
15
+ // Batched String.fromCharCode — avoids the O(n²) cost of repeated += on
16
+ // potentially-large query spans, and stays well under the ~65536 arg limit.
17
+ const n = bytes.length;
18
+ let s = "";
19
+ for (let i = 0; i < n; i += 4096) {
20
+ s += String.fromCharCode(...bytes.subarray(i, Math.min(i + 4096, n)));
21
+ }
22
+ return s;
23
+ }
24
+ /** Perceive input into a content-defined tree (the river fold).
25
+ * Deterministic — identical bytes always produce an identical tree.
26
+ *
27
+ * `boundaries` is an optional sorted list of proper byte offsets where the
28
+ * fold must split so that each prefix segment folds identically to how it
29
+ * folded when it was learned (§10.3 stable-prefix contract). Only the
30
+ * CALLER — who assembled the multi-turn context — knows where those
31
+ * boundaries are; the geometry never guesses them from the bytes. */
32
+ export function perceive(ctx, input, leafAt, lookup, boundaries) {
33
+ if (typeof input === "string" || input instanceof Uint8Array) {
34
+ const bytes = typeof input === "string"
35
+ ? new TextEncoder().encode(input)
36
+ : input;
37
+ if (leafAt === undefined && lookup === undefined) {
38
+ // Per-response memo (see MindContext.perceiveMemo): only the plain
39
+ // inference shape — raw bytes, no store capabilities — is memoised,
40
+ // keyed by CONTENT so byte-identical spans in fresh arrays still hit.
41
+ // The tree is shared by reference; Sema nodes are never mutated.
42
+ const memo = ctx.perceiveMemo;
43
+ if (memo) {
44
+ const key = latin1Key(bytes);
45
+ const hit = memo.get(key);
46
+ if (hit !== undefined)
47
+ return hit;
48
+ const tree = bytesToTree(ctx.space, ctx.alphabet, bytes, undefined, undefined, boundaries);
49
+ memo.set(key, tree);
50
+ return tree;
51
+ }
52
+ return bytesToTree(ctx.space, ctx.alphabet, bytes, undefined, undefined, boundaries);
53
+ }
54
+ return bytesToTree(ctx.space, ctx.alphabet, bytes, leafAt, lookup);
55
+ }
56
+ if (Array.isArray(input)) {
57
+ return gridToTree(ctx.space, ctx.alphabet, stackGrids(input));
58
+ }
59
+ return gridToTree(ctx.space, ctx.alphabet, input);
60
+ }
61
+ /** The DEPOSIT-shaped perceive. A FIRST-SEEN input takes the PLAIN fold
62
+ * (bit-identical to inference perception of a standalone query — that
63
+ * structural train/inference agreement is load-bearing for exact recall),
64
+ * computed incrementally via the fold's level pyramid
65
+ * ({@link bytesToTreePyramid}). An input that EXTENDS a previously
66
+ * deposited one is a conversation context grown by one turn — the cached
67
+ * prefix length IS the turn boundary (derived from the deposit sequence
68
+ * itself, never from content conventions) — and takes the STABLE-PREFIX
69
+ * fold over the accumulated boundaries, bit-identical to the boundary
70
+ * fold query-time conversation perception uses, so the trained context
71
+ * node and the query's context subtree are the SAME node. Segment folds
72
+ * reuse across deposits ({@link stablePrefixFoldIncremental}) — O(turn)
73
+ * instead of O(context) per turn. The fold state is purely a cache; the
74
+ * boundary accumulation is what an evicted chain loses (falling back to
75
+ * the plain fold, the pre-boundary shape — a warm replay restores it). */
76
+ export function perceiveDeposit(ctx, bytes, conversational = false) {
77
+ let prev;
78
+ let prefixLen = 0;
79
+ // Cache consult (both boundary lookup and stable-prefix reuse) is scoped
80
+ // to conversational deposits only — a bare, unrelated fact whose bytes
81
+ // happen to extend an earlier deposit is NOT a conversation turn, and
82
+ // must keep the plain fold so it shares structure with ITS OWN prior
83
+ // deposits, not fragment against a coincidental byte-prefix.
84
+ if (conversational) {
85
+ // Longest cached PROPER prefix first.
86
+ const lens = [...ctx._depositLens]
87
+ .filter((L) => L >= 2 && L < bytes.length)
88
+ .sort((a, b) => b - a);
89
+ for (const L of lens) {
90
+ const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
91
+ // The suffix must bytes-equal the hit's OWN recorded continuation —
92
+ // proof this deposit is that turn's actual next turn, not a fact
93
+ // that coincidentally shares its byte prefix.
94
+ if (hit !== undefined && hit.nextBytes !== undefined &&
95
+ bytesEqual(hit.nextBytes, bytes.subarray(L))) {
96
+ prev = hit;
97
+ prefixLen = L;
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ let tree;
103
+ let entry;
104
+ if (prev !== undefined) {
105
+ const boundaries = [...prev.boundaries, prefixLen];
106
+ const folded = stablePrefixFoldIncremental(ctx.space, ctx.alphabet, bytes, boundaries, prev.stable);
107
+ tree = folded.tree;
108
+ entry = { boundaries, stable: folded.fold };
109
+ }
110
+ else {
111
+ const plain = bytesToTreePyramid(ctx.space, ctx.alphabet, bytes);
112
+ tree = plain.tree;
113
+ entry = { boundaries: [], pyramid: plain.pyramid };
114
+ }
115
+ // Only a conversational deposit writes the cache too — otherwise a bare
116
+ // fact's plain fold could later be misread as a conversation's turn-zero
117
+ // boundary by an unrelated conversational deposit that happens to extend
118
+ // its bytes.
119
+ if (conversational && bytes.length >= 2) {
120
+ // The lengths set drifts as the map evicts; past the probe budget the
121
+ // drift itself becomes the cost (each stale length is an O(len) key
122
+ // build), so both reset together — losing only warm-up on live chains.
123
+ if (ctx._depositLens.size > 64) {
124
+ ctx._depositLens.clear();
125
+ ctx._depositTrees.clear();
126
+ }
127
+ ctx._depositTrees.set(latin1Key(bytes), entry);
128
+ ctx._depositLens.add(bytes.length);
129
+ }
130
+ return tree;
131
+ }
132
+ /** The raw bytes of an input — modality-neutral conversion. */
133
+ export function inputBytes(ctx, input) {
134
+ if (typeof input === "string")
135
+ return new TextEncoder().encode(input);
136
+ if (input instanceof Uint8Array)
137
+ return input;
138
+ if (Array.isArray(input))
139
+ return hilbertBytes(stackGrids(input));
140
+ return hilbertBytes(input);
141
+ }
142
+ /** Convenience: the gist vector of a byte span. */
143
+ export function gistOf(ctx, bytes) {
144
+ return perceive(ctx, bytes).v;
145
+ }
146
+ /** Fold a perceived tree bottom-up against the store's content-addressed maps:
147
+ * every leaf is named by findLeaf, every branch by findBranch over its kids'
148
+ * ids (null the moment any child is unknown). `visit`, when given, sees each
149
+ * node with its byte span and resolved id. Returns the node's byte end and
150
+ * resolved id. */
151
+ export function foldTree(ctx, n, start, visit) {
152
+ // Fast path: subtree already resolved (from a previous conversation turn
153
+ // or an earlier recognition pass). The pyramid reuses prefix subtrees as
154
+ // identical Sema objects, so this cache turns foldTree into O(suffix)
155
+ // instead of O(context) for multi-turn recognition.
156
+ const cached = ctx._resolvedSubtrees?.get(n);
157
+ if (cached !== undefined) {
158
+ const end = start + cached.len;
159
+ visit?.(n, start, end, cached.id);
160
+ return { end, node: cached.id };
161
+ }
162
+ if (n.kids === null) {
163
+ const b = n.leaf ?? new Uint8Array(0);
164
+ const end = start + b.length;
165
+ const node = ctx.store.findLeaf(b);
166
+ visit?.(n, start, end, node);
167
+ if (node !== null && ctx._resolvedSubtrees) {
168
+ ctx._resolvedSubtrees.set(n, { id: node, len: b.length });
169
+ }
170
+ return { end, node };
171
+ }
172
+ let pos = start;
173
+ let known = true;
174
+ const kids = [];
175
+ for (const k of n.kids) {
176
+ const r = foldTree(ctx, k, pos, visit);
177
+ if (r.node === null)
178
+ known = false;
179
+ else if (known)
180
+ kids.push(r.node);
181
+ pos = r.end;
182
+ }
183
+ const node = known ? ctx.store.findBranch(kids) : null;
184
+ visit?.(n, start, pos, node);
185
+ if (node !== null && ctx._resolvedSubtrees) {
186
+ ctx._resolvedSubtrees.set(n, { id: node, len: pos - start });
187
+ }
188
+ return { end: pos, node };
189
+ }
190
+ /** The canonical node id of a byte span: perceive it in isolation — the way
191
+ * training did — and recover its root bottom-up. Returns null if any part is
192
+ * unknown. */
193
+ export function resolve(ctx, bytes) {
194
+ if (bytes.length === 0)
195
+ return null;
196
+ const exact = foldTree(ctx, perceive(ctx, bytes), 0).node;
197
+ if (exact !== null)
198
+ return exact;
199
+ return canonResolve(ctx, bytes);
200
+ }
201
+ /** Equivalence-class resolution: when the exact content-addressed lookup
202
+ * misses, find a stored node whose CANONICAL key equals the span's — the
203
+ * store's canon index proposes candidates by key hash, and each is verified
204
+ * by re-canonicalizing its bytes (hash-then-verify, like every content
205
+ * lookup). Among verified candidates, one that leads somewhere (has a
206
+ * continuation edge) is preferred; ties break to the lowest id — a corpus
207
+ * property, not a seed property. Null when the response carries no
208
+ * canonicalizer, the store has no canon index, or nothing verifies. */
209
+ export function canonResolve(ctx, bytes) {
210
+ const canon = ctx.canon;
211
+ const store = ctx.store;
212
+ if (canon === null || !store.canonFind)
213
+ return null;
214
+ if (bytes.length < 2)
215
+ return null;
216
+ const memo = ctx.canonMemo;
217
+ const memoKey = memo ? latin1Key(bytes) : "";
218
+ if (memo) {
219
+ const hit = memo.get(memoKey);
220
+ if (hit !== undefined)
221
+ return hit;
222
+ }
223
+ const set = (v) => {
224
+ memo?.set(memoKey, v);
225
+ return v;
226
+ };
227
+ const key = canon(bytes);
228
+ if (key.length === 0)
229
+ return set(null);
230
+ // A stored form that IS canonical is not in the index (buildCanonIndex
231
+ // skips identity rows) — the exact content-addressed lookup of the
232
+ // canonical bytes finds it directly.
233
+ if (key.length !== bytes.length || !bytesEqual(key, bytes)) {
234
+ const direct = foldTree(ctx, perceive(ctx, key), 0).node;
235
+ if (direct !== null)
236
+ return set(direct);
237
+ }
238
+ const candidates = store.canonFind(canonHash(key));
239
+ if (candidates.length === 0)
240
+ return set(null);
241
+ let best = null;
242
+ let bestLeads = false;
243
+ for (const id of candidates) {
244
+ const bytesOf = read(ctx, id);
245
+ const stored = canon(bytesOf);
246
+ if (stored.length !== key.length || !bytesEqual(stored, key))
247
+ continue;
248
+ // The index stores FLAT content twins; the id the exact path would have
249
+ // resolved for these bytes is their FOLD — the deposit-shaped node that
250
+ // carries the edges and halos. Re-folding the candidate's bytes lands
251
+ // on exactly the node the canonical-case query would have found.
252
+ const folded = foldTree(ctx, perceive(ctx, bytesOf), 0).node;
253
+ const use = folded ?? id;
254
+ const leads = store.hasNext(use) || store.haloMass(use) > 0;
255
+ if (best === null || (leads && !bestLeads) ||
256
+ (leads === bestLeads && use < best)) {
257
+ best = use;
258
+ bestLeads = leads;
259
+ }
260
+ }
261
+ return set(best);
262
+ }
263
+ /** Walk a perceived tree in POST-ORDER with byte offsets — children before
264
+ * their parent, `visit(node, start, end)` for every node including leaves.
265
+ * Returns the byte end. The one shared traversal the offset-carrying tree
266
+ * readers (recognition via foldTree's richer variant, attention's region
267
+ * collection, resonance's branch counting) build on, so each does not
268
+ * re-derive the offset bookkeeping. (recognition.segment keeps its own
269
+ * walk: its flush semantics need PRE-order decisions at leaf-parents, which
270
+ * a post-order visitor cannot express.) */
271
+ export function walkTree(n, start, visit) {
272
+ if (n.kids === null) {
273
+ const end = start + (n.leaf?.length ?? 0);
274
+ visit(n, start, end);
275
+ return end;
276
+ }
277
+ let pos = start;
278
+ for (const k of n.kids)
279
+ pos = walkTree(k, pos, visit);
280
+ visit(n, start, pos);
281
+ return pos;
282
+ }
283
+ // ── Read: node → bytes ──────────────────────────────────────────────────
284
+ /** Reconstruct a node's byte content from the DAG, up to `maxLen` bytes. */
285
+ export function read(ctx, id, maxLen = ALL) {
286
+ return ctx.store.bytesPrefix(id, maxLen);
287
+ }
@@ -0,0 +1,139 @@
1
+ import type { Vec } from "../vec.js";
2
+ /** One element of a step's input or output vector.
3
+ *
4
+ * Modality-neutral and deliberately partial: an element might be a byte span of
5
+ * the query, a resolved graph node, a resonance hit with its score, a spliced
6
+ * connector — so every descriptive field is optional and a mechanism fills only
7
+ * the ones that carry meaning for what it did. `text` is always present (the
8
+ * human-readable rendering); the rest is provenance a debugger can lean on. */
9
+ export interface RationaleItem {
10
+ /** Human-readable rendering — decoded text for a byte span, else a label like
11
+ * "‹none›" or an operator name. Always set, so a step always reads. */
12
+ text: string;
13
+ /** The graph node this element is, or resolved to, when known — the handle to
14
+ * point back at the exact stored fact in the content-addressed DAG. */
15
+ node?: number;
16
+ /** The `[start, end)` span this element occupies in its step's frame of
17
+ * reference (usually the query or the answer being composed). */
18
+ span?: [number, number];
19
+ /** The resonance / cosine score that selected this element, when it was chosen
20
+ * by similarity rather than by exact structure. */
21
+ score?: number;
22
+ /** A short role tag — "query", "leaf", "form", "hit", "connector", "answer",
23
+ * … — naming what KIND of element this is within the step. */
24
+ role?: string;
25
+ /** The gist vector, only when the element fundamentally IS a vector and a
26
+ * caller asked to carry it (off by default — a D-float array per item would
27
+ * bury the reasoning it is meant to explain). */
28
+ v?: Vec;
29
+ }
30
+ /** A single completed act of inference — one mechanism, run once.
31
+ *
32
+ * Steps are emitted in COMPLETION order (a sub-mechanism finishes, and is
33
+ * reported, before the mechanism that called it), while `index` is assigned in
34
+ * ENTRY order (a parent reserves its index before its children run). So a
35
+ * parent's `index` is always lower than its children's, and the two orderings
36
+ * together give a valid topological reading of the dependency graph. */
37
+ export interface RationaleStep {
38
+ /** This step's index, assigned when the mechanism was ENTERED — a strict,
39
+ * incremental ordering over the whole inference. */
40
+ index: number;
41
+ /** The mechanism and its enclosing mechanisms, outermost → innermost, e.g.
42
+ * `["respond", "think", "recognise"]`. The last entry is this step; the
43
+ * prefix is the nest of sub-mechanisms it ran inside. */
44
+ mechanism: string[];
45
+ /** The enclosing mechanism's step index, or -1 at the root — the NESTING edge
46
+ * of the dependency graph (which step this one is a part of). */
47
+ parent: number;
48
+ /** The earlier steps whose OUTPUTS became this step's inputs — the DATA-FLOW
49
+ * edges of the dependency graph. Defaults to the previous sibling (the step
50
+ * run just before this one inside the same mechanism), or the parent when
51
+ * this is the first sub-step; a mechanism that fuses several earlier results
52
+ * names them all explicitly. */
53
+ dependsOn: number[];
54
+ /** The vector of elements handed to the mechanism (one or more). */
55
+ inputs: RationaleItem[];
56
+ /** The vector of elements the mechanism produced (one or more) — longer than
57
+ * `inputs` when it decomposed, shorter when it combined. */
58
+ outputs: RationaleItem[];
59
+ /** A one-line, human account of what the mechanism did and why — the sentence
60
+ * that turns the data into an explanation. */
61
+ note?: string;
62
+ /** Optional structured payload — a mechanism-specific, plain-serialisable
63
+ * shape (no Map/Set/vectors/mutable internals) that carries more than the
64
+ * human-readable `note` can, for a debugger or downstream tool to consume
65
+ * programmatically. Never read by inference; purely additive. */
66
+ data?: unknown;
67
+ }
68
+ /** The callback {@link Mind.respond} / {@link Mind.respondText} accept. It is
69
+ * invoked once per completed step, AS the inference unfolds — never batched at
70
+ * the end — so a caller can stream the reasoning live or accumulate it. */
71
+ export type InspectRationale = (step: RationaleStep) => void;
72
+ /** Decode bytes to text for display, dropping the NUL padding the encoder uses
73
+ * (the same cleanup {@link Mind.respondText} does for its result). */
74
+ export declare function decodeText(bytes: Uint8Array): string;
75
+ /** The `[start, end)` gaps of `[0, queryLen)` NOT covered by `accounted` —
76
+ * the same union-of-spans reading think's grounding decider prices at PASS
77
+ * per byte, exposed here so a mechanism can turn it into a human label. */
78
+ export declare function unexplainedSpans(queryLen: number, accounted: ReadonlyArray<[number, number]>): Array<[number, number]>;
79
+ /** A human-readable label for the query bytes a mechanism's `accounted`
80
+ * spans leave unexplained — purely diagnostic (Task 2's negative evidence):
81
+ * it never changes a candidate's weight, only what the rationale trace
82
+ * says the mechanism left on the table. `""` when nothing is unexplained. */
83
+ export declare function unexplainedLabel(query: Uint8Array, accounted: ReadonlyArray<[number, number]>): string;
84
+ /** An open mechanism — the handle {@link Rationale.enter} returns. Hold it for
85
+ * the duration of the mechanism and call {@link Scope.done} with the outputs
86
+ * when it finishes; that emits the step and pops the nesting. */
87
+ export interface Scope {
88
+ /** The step index reserved for this mechanism at entry — pass it as an
89
+ * explicit dependency of a later step that consumes this one's output. */
90
+ readonly index: number;
91
+ /** Close the mechanism: emit its step with these outputs and pop it off the
92
+ * nesting stack. Idempotent — a second call is ignored, so a `finally` that
93
+ * closes after an early return is safe. */
94
+ done(outputs: RationaleItem[], note?: string, data?: unknown): void;
95
+ }
96
+ /** The live tracer: a stack of open mechanisms over one {@link Mind.respond}.
97
+ *
98
+ * Sema's inference is single-threaded and strictly sequential — every async
99
+ * step is awaited before the next begins, and `respond` holds no two thoughts
100
+ * at once — so a plain stack exactly tracks the current nesting: {@link enter}
101
+ * pushes, {@link Scope.done} pops, and {@link step} (a mechanism with no
102
+ * sub-steps) is the two fused. The tracer never branches the control flow; it
103
+ * only records it. */
104
+ export declare class Rationale {
105
+ private readonly sink;
106
+ private next;
107
+ /** Open mechanisms, outermost first. Each frame remembers the last child it
108
+ * has spawned so the next sibling can default its data-flow edge to it. */
109
+ private readonly stack;
110
+ /** The most recent step index emitted under each mechanism name — the handle
111
+ * a later step uses to name an EARLIER mechanism as its data-flow producer
112
+ * (e.g. cover depends on the latest recognise / computeExtensions). One tracer is
113
+ * built per response and inference is sequential, so "most recent" is exactly
114
+ * "the one that produced the inputs I am about to consume". */
115
+ private readonly lastByName;
116
+ constructor(sink: InspectRationale);
117
+ /** The index of the most recent step with this mechanism name, or undefined if
118
+ * none has run. Used to wire an explicit producer edge into {@link
119
+ * Scope.done} / {@link step}'s `deps`. */
120
+ lastIndex(name: string): number | undefined;
121
+ /** The mechanism names currently open, outermost → innermost. */
122
+ private path;
123
+ /** The default data-flow edge for a step entering now: the previous sibling
124
+ * inside the current mechanism, else the enclosing mechanism, else nothing
125
+ * (the root). An explicit `deps` overrides this. */
126
+ private defaultDeps;
127
+ /** Reserve this step's index and register it as the current mechanism's most
128
+ * recent child (so the NEXT sibling chains to it) and as the most recent step
129
+ * of its own NAME (so a later mechanism can name it as a producer). */
130
+ private reserve;
131
+ private emit;
132
+ /** Enter a mechanism that has sub-steps. Captures its inputs and the nesting
133
+ * now; the matching {@link Scope.done} supplies the outputs when it finishes.
134
+ * `deps` overrides the default data-flow edge (previous sibling / parent). */
135
+ enter(name: string, inputs: RationaleItem[], deps?: number[]): Scope;
136
+ /** Record a mechanism that has no sub-steps — its inputs and outputs are both
137
+ * known at the call site. Returns its index, for a later step to depend on. */
138
+ step(name: string, inputs: RationaleItem[], outputs: RationaleItem[], note?: string, deps?: number[], data?: unknown): number;
139
+ }
@@ -0,0 +1,163 @@
1
+ // rationale.ts — the inference, told as it happens.
2
+ //
3
+ // Sema's edge over a weight matrix is that every answer is a DERIVATION over
4
+ // explicit facts, not a sample from an opaque distribution. This module turns
5
+ // that derivation into a stream a human (or a debugger) can read: as {@link
6
+ // Mind.respond} thinks, each inference MECHANISM it runs emits a {@link
7
+ // RationaleStep} the moment it completes — what it was handed, what it produced,
8
+ // where it sits in the nesting of mechanisms, and which earlier steps fed it.
9
+ //
10
+ // Nothing here drives the inference; it only WITNESSES it. When no
11
+ // `inspectRationale` callback is supplied the tracer is never constructed and
12
+ // the cost is exactly zero — every emit site in src/mind/mind.ts is guarded by `?.`, and
13
+ // optional-chaining short-circuits its arguments, so the items are not even
14
+ // built (see {@link Mind.respond}).
15
+ //
16
+ // The shape of a step mirrors how Sema reasons. A mechanism is rarely a 1→1
17
+ // map: {@link Mind.recognise} DECOMPOSES one query into many recognised forms;
18
+ // the cover COMBINES many forms back into one answer; resonance fans one gist
19
+ // out into a ranked list of hits. So a step's `inputs` and `outputs` are each a
20
+ // VECTOR — an ordered list of {@link RationaleItem}s, one per element — and the
21
+ // fan-out / fan-in is visible in their lengths.
22
+ /** Decode bytes to text for display, dropping the NUL padding the encoder uses
23
+ * (the same cleanup {@link Mind.respondText} does for its result). */
24
+ export function decodeText(bytes) {
25
+ return new TextDecoder().decode(bytes.filter((b) => b !== 0x00));
26
+ }
27
+ /** The `[start, end)` gaps of `[0, queryLen)` NOT covered by `accounted` —
28
+ * the same union-of-spans reading think's grounding decider prices at PASS
29
+ * per byte, exposed here so a mechanism can turn it into a human label. */
30
+ export function unexplainedSpans(queryLen, accounted) {
31
+ const sorted = accounted
32
+ .map(([s, e]) => [Math.max(0, s), Math.min(queryLen, e)])
33
+ .filter(([s, e]) => e > s)
34
+ .sort((a, b) => a[0] - b[0]);
35
+ const gaps = [];
36
+ let reach = 0;
37
+ for (const [s, e] of sorted) {
38
+ if (s > reach)
39
+ gaps.push([reach, s]);
40
+ if (e > reach)
41
+ reach = e;
42
+ }
43
+ if (reach < queryLen)
44
+ gaps.push([reach, queryLen]);
45
+ return gaps;
46
+ }
47
+ /** A human-readable label for the query bytes a mechanism's `accounted`
48
+ * spans leave unexplained — purely diagnostic (Task 2's negative evidence):
49
+ * it never changes a candidate's weight, only what the rationale trace
50
+ * says the mechanism left on the table. `""` when nothing is unexplained. */
51
+ export function unexplainedLabel(query, accounted) {
52
+ const gaps = unexplainedSpans(query.length, accounted);
53
+ if (gaps.length === 0)
54
+ return "";
55
+ return gaps.map(([s, e]) => decodeText(query.subarray(s, e))).join(" … ");
56
+ }
57
+ /** The live tracer: a stack of open mechanisms over one {@link Mind.respond}.
58
+ *
59
+ * Sema's inference is single-threaded and strictly sequential — every async
60
+ * step is awaited before the next begins, and `respond` holds no two thoughts
61
+ * at once — so a plain stack exactly tracks the current nesting: {@link enter}
62
+ * pushes, {@link Scope.done} pops, and {@link step} (a mechanism with no
63
+ * sub-steps) is the two fused. The tracer never branches the control flow; it
64
+ * only records it. */
65
+ export class Rationale {
66
+ sink;
67
+ next = 0;
68
+ /** Open mechanisms, outermost first. Each frame remembers the last child it
69
+ * has spawned so the next sibling can default its data-flow edge to it. */
70
+ stack = [];
71
+ /** The most recent step index emitted under each mechanism name — the handle
72
+ * a later step uses to name an EARLIER mechanism as its data-flow producer
73
+ * (e.g. cover depends on the latest recognise / computeExtensions). One tracer is
74
+ * built per response and inference is sequential, so "most recent" is exactly
75
+ * "the one that produced the inputs I am about to consume". */
76
+ lastByName = new Map();
77
+ constructor(sink) {
78
+ this.sink = sink;
79
+ }
80
+ /** The index of the most recent step with this mechanism name, or undefined if
81
+ * none has run. Used to wire an explicit producer edge into {@link
82
+ * Scope.done} / {@link step}'s `deps`. */
83
+ lastIndex(name) {
84
+ return this.lastByName.get(name);
85
+ }
86
+ /** The mechanism names currently open, outermost → innermost. */
87
+ path(leaf) {
88
+ const p = this.stack.map((f) => f.name);
89
+ p.push(leaf);
90
+ return p;
91
+ }
92
+ /** The default data-flow edge for a step entering now: the previous sibling
93
+ * inside the current mechanism, else the enclosing mechanism, else nothing
94
+ * (the root). An explicit `deps` overrides this. */
95
+ defaultDeps() {
96
+ const top = this.stack[this.stack.length - 1];
97
+ if (!top)
98
+ return [];
99
+ return [top.lastChild ?? top.index];
100
+ }
101
+ /** Reserve this step's index and register it as the current mechanism's most
102
+ * recent child (so the NEXT sibling chains to it) and as the most recent step
103
+ * of its own NAME (so a later mechanism can name it as a producer). */
104
+ reserve(name) {
105
+ const index = this.next++;
106
+ const top = this.stack[this.stack.length - 1];
107
+ if (top)
108
+ top.lastChild = index;
109
+ this.lastByName.set(name, index);
110
+ return index;
111
+ }
112
+ emit(index, mechanism, inputs, outputs, deps, note, data) {
113
+ this.sink({
114
+ index,
115
+ mechanism,
116
+ parent: this.stack.length > 0
117
+ ? this.stack[this.stack.length - 1].index
118
+ : -1,
119
+ dependsOn: deps ?? this.defaultDeps(),
120
+ inputs,
121
+ outputs,
122
+ note,
123
+ data,
124
+ });
125
+ }
126
+ /** Enter a mechanism that has sub-steps. Captures its inputs and the nesting
127
+ * now; the matching {@link Scope.done} supplies the outputs when it finishes.
128
+ * `deps` overrides the default data-flow edge (previous sibling / parent). */
129
+ enter(name, inputs, deps) {
130
+ const mechanism = this.path(name);
131
+ const resolvedDeps = deps ?? this.defaultDeps();
132
+ const index = this.reserve(name);
133
+ this.stack.push({ index, name, lastChild: null });
134
+ let closed = false;
135
+ const emit = this.emit.bind(this);
136
+ const pop = () => {
137
+ // Pop down to and including this frame — tolerant of a sub-mechanism that
138
+ // forgot to close, so one missed `done` cannot desync the whole stack.
139
+ const at = this.stack.findIndex((f) => f.index === index);
140
+ if (at >= 0)
141
+ this.stack.length = at;
142
+ };
143
+ return {
144
+ index,
145
+ done: (outputs, note, data) => {
146
+ if (closed)
147
+ return;
148
+ closed = true;
149
+ pop();
150
+ emit(index, mechanism, inputs, outputs, resolvedDeps, note, data);
151
+ },
152
+ };
153
+ }
154
+ /** Record a mechanism that has no sub-steps — its inputs and outputs are both
155
+ * known at the call site. Returns its index, for a later step to depend on. */
156
+ step(name, inputs, outputs, note, deps, data) {
157
+ const mechanism = this.path(name);
158
+ const resolvedDeps = deps ?? this.defaultDeps();
159
+ const index = this.reserve(name);
160
+ this.emit(index, mechanism, inputs, outputs, resolvedDeps, note, data);
161
+ return index;
162
+ }
163
+ }
@@ -0,0 +1,34 @@
1
+ import type { MindContext } from "./types.js";
2
+ import type { Precomputed } from "./pipeline-mechanism.js";
3
+ /** Whether `bytes` is a proper byte-subspan of `query` — already present in
4
+ * the question, so voicing it back only restates part of what was asked,
5
+ * never answers it. The exact guard recallByResonance already applies to
6
+ * its OWN grounding candidates (tier 1's `restates`, tier 2's subspan
7
+ * check, tier 0b's argument-binding subspan check) — every mechanism that
8
+ * walks a LEARNT CONTINUATION EDGE past an already-vetted grounding
9
+ * (reason()'s own hops below, and CAST's `projectCounterfactual` seat
10
+ * substitution — see cast.ts) needs the same guard applied to what the
11
+ * walk turns up, since `follow()`/`chooseNext`/`pivotInto` know nothing of
12
+ * the query at all — only of what structurally continues what. */
13
+ export declare function restatesQuery(query: Uint8Array, bytes: Uint8Array): boolean;
14
+ /** Extend a grounded answer forward across facts (multi-hop reasoning).
15
+ * Pivots on the longest unconsumed learnt context each answer contains,
16
+ * then follows the pivot's continuation to the next fact. Repeats up
17
+ * to `cfg.recallQueryK` hops. `preConsumed` carries node ids already
18
+ * spoken for by the grounding stage (cover/extract/CAST). `pre` is the
19
+ * response's shared pre-computation — the post-grounding stages read the
20
+ * same container the mechanisms did. */
21
+ export declare function reason(ctx: MindContext, query: Uint8Array, answer: Uint8Array, preConsumed: ReadonlySet<number>, pre: Precomputed): Promise<Uint8Array>;
22
+ /** Fuse independent points of attention into one answer (multi-topic).
23
+ * When the consensus climb finds more than one dominant point, each
24
+ * independent point grounds its own answer; they are bridged together
25
+ * by any learnt connector the graph holds between them. */
26
+ export declare function fuseAttention(ctx: MindContext, query: Uint8Array, primary: Uint8Array, pre: Precomputed,
27
+ /** True when `primary` never touched the consensus climb at all — e.g. a
28
+ * pure ALU computation, which has no anchor of its own. commitVotes
29
+ * ALWAYS admits the dominant root regardless of its vote (attention.ts:
30
+ * "roots.length === 0 || …") on the assumption a lone root already IS
31
+ * primary's own source; that assumption is exactly backwards when
32
+ * primary is unclimbed. Absent or false preserves the original
33
+ * behaviour exactly. */
34
+ unclimbed?: boolean): Promise<Uint8Array>;