@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,201 @@
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
+
6
+ import { Vec } from "../vec.js";
7
+ import { Sema, Space } from "../sema.js";
8
+ import { Alphabet } from "../alphabet.js";
9
+ import {
10
+ bytesToTree,
11
+ bytesToTreePyramid,
12
+ type FoldPyramid,
13
+ Grid,
14
+ gridToTree,
15
+ hilbertBytes,
16
+ stackGrids,
17
+ } from "../geometry.js";
18
+ import { ALL } from "./types.js";
19
+ import type { Input, MindContext } from "./types.js";
20
+
21
+ // ── Address: bytes → node ──────────────────────────────────────────────
22
+
23
+ /** The content key of a byte span — one latin1 char per byte, an exact,
24
+ * collision-free encoding. Spans on the perception path are query-scale
25
+ * (windows, regions, candidate spans), so key construction is far cheaper
26
+ * than the river fold it deduplicates. */
27
+ function latin1Key(bytes: Uint8Array): string {
28
+ // Batched String.fromCharCode — avoids the O(n²) cost of repeated += on
29
+ // potentially-large query spans, and stays well under the ~65536 arg limit.
30
+ const n = bytes.length;
31
+ let s = "";
32
+ for (let i = 0; i < n; i += 4096) {
33
+ s += String.fromCharCode(...bytes.subarray(i, Math.min(i + 4096, n)));
34
+ }
35
+ return s;
36
+ }
37
+
38
+ /** Perceive input into a content-defined tree (the river fold).
39
+ * Deterministic — identical bytes always produce an identical tree. */
40
+ export function perceive(
41
+ ctx: MindContext,
42
+ input: Input,
43
+ leafAt?: (i: number) => number | null,
44
+ lookup?: (ids: number[]) => number | null,
45
+ ): Sema {
46
+ if (typeof input === "string" || input instanceof Uint8Array) {
47
+ const bytes = typeof input === "string"
48
+ ? new TextEncoder().encode(input)
49
+ : input;
50
+ if (leafAt === undefined && lookup === undefined) {
51
+ // Per-response memo (see MindContext.perceiveMemo): only the plain
52
+ // inference shape — raw bytes, no store capabilities — is memoised,
53
+ // keyed by CONTENT so byte-identical spans in fresh arrays still hit.
54
+ // The tree is shared by reference; Sema nodes are never mutated.
55
+ const memo = ctx.perceiveMemo;
56
+ if (memo) {
57
+ const key = latin1Key(bytes);
58
+ const hit = memo.get(key);
59
+ if (hit !== undefined) return hit;
60
+ const tree = bytesToTree(ctx.space, ctx.alphabet, bytes);
61
+ memo.set(key, tree);
62
+ return tree;
63
+ }
64
+ return bytesToTree(ctx.space, ctx.alphabet, bytes);
65
+ }
66
+ return bytesToTree(ctx.space, ctx.alphabet, bytes, leafAt, lookup);
67
+ }
68
+ if (Array.isArray(input)) {
69
+ return gridToTree(ctx.space, ctx.alphabet, stackGrids(input));
70
+ }
71
+ return gridToTree(ctx.space, ctx.alphabet, input as Grid);
72
+ }
73
+
74
+ /** The DEPOSIT-shaped perceive: the PLAIN fold (bit-identical to inference
75
+ * perception — that structural train/inference agreement is load-bearing
76
+ * for exact recall), computed INCREMENTALLY via the fold's level pyramid
77
+ * ({@link bytesToTreePyramid}). An accumulated context (a conversation)
78
+ * grows by suffixes: the previous context's pyramid is cached by CONTENT
79
+ * (ctx._depositTrees), and this deposit refolds only the right edge of
80
+ * each level — O(turn) instead of O(context) per turn. Purely a cache:
81
+ * the produced tree never depends on cache state. */
82
+ export function perceiveDeposit(ctx: MindContext, bytes: Uint8Array): Sema {
83
+ let prev: FoldPyramid | undefined;
84
+ // Longest cached PROPER prefix first.
85
+ const lens = [...ctx._depositLens]
86
+ .filter((L) => L >= 2 && L < bytes.length)
87
+ .sort((a, b) => b - a);
88
+ for (const L of lens) {
89
+ const hit = ctx._depositTrees.get(latin1Key(bytes.subarray(0, L)));
90
+ if (hit !== undefined) {
91
+ prev = hit;
92
+ break;
93
+ }
94
+ }
95
+ const { tree, pyramid } = bytesToTreePyramid(
96
+ ctx.space,
97
+ ctx.alphabet,
98
+ bytes,
99
+ prev,
100
+ );
101
+ if (bytes.length >= 2) {
102
+ // The lengths set drifts as the map evicts; past the probe budget the
103
+ // drift itself becomes the cost (each stale length is an O(len) key
104
+ // build), so both reset together — losing only warm-up on live chains.
105
+ if (ctx._depositLens.size > 64) {
106
+ ctx._depositLens.clear();
107
+ ctx._depositTrees.clear();
108
+ }
109
+ ctx._depositTrees.set(latin1Key(bytes), pyramid);
110
+ ctx._depositLens.add(bytes.length);
111
+ }
112
+ return tree;
113
+ }
114
+
115
+ /** The raw bytes of an input — modality-neutral conversion. */
116
+ export function inputBytes(ctx: MindContext, input: Input): Uint8Array {
117
+ if (typeof input === "string") return new TextEncoder().encode(input);
118
+ if (input instanceof Uint8Array) return input;
119
+ if (Array.isArray(input)) return hilbertBytes(stackGrids(input));
120
+ return hilbertBytes(input as Grid);
121
+ }
122
+
123
+ /** Convenience: the gist vector of a byte span. */
124
+ export function gistOf(ctx: MindContext, bytes: Uint8Array): Vec {
125
+ return perceive(ctx, bytes).v;
126
+ }
127
+
128
+ /** Fold a perceived tree bottom-up against the store's content-addressed maps:
129
+ * every leaf is named by findLeaf, every branch by findBranch over its kids'
130
+ * ids (null the moment any child is unknown). `visit`, when given, sees each
131
+ * node with its byte span and resolved id. Returns the node's byte end and
132
+ * resolved id. */
133
+ export function foldTree(
134
+ ctx: MindContext,
135
+ n: Sema,
136
+ start: number,
137
+ visit?: (n: Sema, start: number, end: number, node: number | null) => void,
138
+ ): { end: number; node: number | null } {
139
+ if (n.kids === null) {
140
+ const b = n.leaf ?? new Uint8Array(0);
141
+ const end = start + b.length;
142
+ const node = ctx.store.findLeaf(b);
143
+ visit?.(n, start, end, node);
144
+ return { end, node };
145
+ }
146
+ let pos = start;
147
+ let known = true;
148
+ const kids: number[] = [];
149
+ for (const k of n.kids) {
150
+ const r = foldTree(ctx, k, pos, visit);
151
+ if (r.node === null) known = false;
152
+ else if (known) kids.push(r.node);
153
+ pos = r.end;
154
+ }
155
+ const node = known ? ctx.store.findBranch(kids) : null;
156
+ visit?.(n, start, pos, node);
157
+ return { end: pos, node };
158
+ }
159
+
160
+ /** The canonical node id of a byte span: perceive it in isolation — the way
161
+ * training did — and recover its root bottom-up. Returns null if any part is
162
+ * unknown. */
163
+ export function resolve(ctx: MindContext, bytes: Uint8Array): number | null {
164
+ if (bytes.length === 0) return null;
165
+ return foldTree(ctx, perceive(ctx, bytes), 0).node;
166
+ }
167
+
168
+ /** Walk a perceived tree in POST-ORDER with byte offsets — children before
169
+ * their parent, `visit(node, start, end)` for every node including leaves.
170
+ * Returns the byte end. The one shared traversal the offset-carrying tree
171
+ * readers (recognition via foldTree's richer variant, attention's region
172
+ * collection, resonance's branch counting) build on, so each does not
173
+ * re-derive the offset bookkeeping. (recognition.segment keeps its own
174
+ * walk: its flush semantics need PRE-order decisions at leaf-parents, which
175
+ * a post-order visitor cannot express.) */
176
+ export function walkTree(
177
+ n: Sema,
178
+ start: number,
179
+ visit: (node: Sema, start: number, end: number) => void,
180
+ ): number {
181
+ if (n.kids === null) {
182
+ const end = start + (n.leaf?.length ?? 0);
183
+ visit(n, start, end);
184
+ return end;
185
+ }
186
+ let pos = start;
187
+ for (const k of n.kids) pos = walkTree(k, pos, visit);
188
+ visit(n, start, pos);
189
+ return pos;
190
+ }
191
+
192
+ // ── Read: node → bytes ──────────────────────────────────────────────────
193
+
194
+ /** Reconstruct a node's byte content from the DAG, up to `maxLen` bytes. */
195
+ export function read(
196
+ ctx: MindContext,
197
+ id: number,
198
+ maxLen: number = ALL,
199
+ ): Uint8Array {
200
+ return ctx.store.bytesPrefix(id, maxLen);
201
+ }
@@ -0,0 +1,275 @@
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
+
23
+ import type { Vec } from "../vec.js";
24
+
25
+ /** One element of a step's input or output vector.
26
+ *
27
+ * Modality-neutral and deliberately partial: an element might be a byte span of
28
+ * the query, a resolved graph node, a resonance hit with its score, a spliced
29
+ * connector — so every descriptive field is optional and a mechanism fills only
30
+ * the ones that carry meaning for what it did. `text` is always present (the
31
+ * human-readable rendering); the rest is provenance a debugger can lean on. */
32
+ export interface RationaleItem {
33
+ /** Human-readable rendering — decoded text for a byte span, else a label like
34
+ * "‹none›" or an operator name. Always set, so a step always reads. */
35
+ text: string;
36
+ /** The graph node this element is, or resolved to, when known — the handle to
37
+ * point back at the exact stored fact in the content-addressed DAG. */
38
+ node?: number;
39
+ /** The `[start, end)` span this element occupies in its step's frame of
40
+ * reference (usually the query or the answer being composed). */
41
+ span?: [number, number];
42
+ /** The resonance / cosine score that selected this element, when it was chosen
43
+ * by similarity rather than by exact structure. */
44
+ score?: number;
45
+ /** A short role tag — "query", "leaf", "form", "hit", "connector", "answer",
46
+ * … — naming what KIND of element this is within the step. */
47
+ role?: string;
48
+ /** The gist vector, only when the element fundamentally IS a vector and a
49
+ * caller asked to carry it (off by default — a D-float array per item would
50
+ * bury the reasoning it is meant to explain). */
51
+ v?: Vec;
52
+ }
53
+
54
+ /** A single completed act of inference — one mechanism, run once.
55
+ *
56
+ * Steps are emitted in COMPLETION order (a sub-mechanism finishes, and is
57
+ * reported, before the mechanism that called it), while `index` is assigned in
58
+ * ENTRY order (a parent reserves its index before its children run). So a
59
+ * parent's `index` is always lower than its children's, and the two orderings
60
+ * together give a valid topological reading of the dependency graph. */
61
+ export interface RationaleStep {
62
+ /** This step's index, assigned when the mechanism was ENTERED — a strict,
63
+ * incremental ordering over the whole inference. */
64
+ index: number;
65
+ /** The mechanism and its enclosing mechanisms, outermost → innermost, e.g.
66
+ * `["respond", "think", "recognise"]`. The last entry is this step; the
67
+ * prefix is the nest of sub-mechanisms it ran inside. */
68
+ mechanism: string[];
69
+ /** The enclosing mechanism's step index, or -1 at the root — the NESTING edge
70
+ * of the dependency graph (which step this one is a part of). */
71
+ parent: number;
72
+ /** The earlier steps whose OUTPUTS became this step's inputs — the DATA-FLOW
73
+ * edges of the dependency graph. Defaults to the previous sibling (the step
74
+ * run just before this one inside the same mechanism), or the parent when
75
+ * this is the first sub-step; a mechanism that fuses several earlier results
76
+ * names them all explicitly. */
77
+ dependsOn: number[];
78
+ /** The vector of elements handed to the mechanism (one or more). */
79
+ inputs: RationaleItem[];
80
+ /** The vector of elements the mechanism produced (one or more) — longer than
81
+ * `inputs` when it decomposed, shorter when it combined. */
82
+ outputs: RationaleItem[];
83
+ /** A one-line, human account of what the mechanism did and why — the sentence
84
+ * that turns the data into an explanation. */
85
+ note?: string;
86
+ }
87
+
88
+ /** The callback {@link Mind.respond} / {@link Mind.respondText} accept. It is
89
+ * invoked once per completed step, AS the inference unfolds — never batched at
90
+ * the end — so a caller can stream the reasoning live or accumulate it. */
91
+ export type InspectRationale = (step: RationaleStep) => void;
92
+
93
+ /** Decode bytes to text for display, dropping the NUL padding the encoder uses
94
+ * (the same cleanup {@link Mind.respondText} does for its result). */
95
+ export function decodeText(bytes: Uint8Array): string {
96
+ return new TextDecoder().decode(bytes.filter((b) => b !== 0x00));
97
+ }
98
+
99
+ /** The `[start, end)` gaps of `[0, queryLen)` NOT covered by `accounted` —
100
+ * the same union-of-spans reading think's grounding decider prices at PASS
101
+ * per byte, exposed here so a mechanism can turn it into a human label. */
102
+ export function unexplainedSpans(
103
+ queryLen: number,
104
+ accounted: ReadonlyArray<[number, number]>,
105
+ ): Array<[number, number]> {
106
+ const sorted = accounted
107
+ .map(([s, e]) =>
108
+ [Math.max(0, s), Math.min(queryLen, e)] as [number, number]
109
+ )
110
+ .filter(([s, e]) => e > s)
111
+ .sort((a, b) => a[0] - b[0]);
112
+ const gaps: Array<[number, number]> = [];
113
+ let reach = 0;
114
+ for (const [s, e] of sorted) {
115
+ if (s > reach) gaps.push([reach, s]);
116
+ if (e > reach) reach = e;
117
+ }
118
+ if (reach < queryLen) gaps.push([reach, queryLen]);
119
+ return gaps;
120
+ }
121
+
122
+ /** A human-readable label for the query bytes a mechanism's `accounted`
123
+ * spans leave unexplained — purely diagnostic (Task 2's negative evidence):
124
+ * it never changes a candidate's weight, only what the rationale trace
125
+ * says the mechanism left on the table. `""` when nothing is unexplained. */
126
+ export function unexplainedLabel(
127
+ query: Uint8Array,
128
+ accounted: ReadonlyArray<[number, number]>,
129
+ ): string {
130
+ const gaps = unexplainedSpans(query.length, accounted);
131
+ if (gaps.length === 0) return "";
132
+ return gaps.map(([s, e]) => decodeText(query.subarray(s, e))).join(" … ");
133
+ }
134
+
135
+ /** An open mechanism — the handle {@link Rationale.enter} returns. Hold it for
136
+ * the duration of the mechanism and call {@link Scope.done} with the outputs
137
+ * when it finishes; that emits the step and pops the nesting. */
138
+ export interface Scope {
139
+ /** The step index reserved for this mechanism at entry — pass it as an
140
+ * explicit dependency of a later step that consumes this one's output. */
141
+ readonly index: number;
142
+ /** Close the mechanism: emit its step with these outputs and pop it off the
143
+ * nesting stack. Idempotent — a second call is ignored, so a `finally` that
144
+ * closes after an early return is safe. */
145
+ done(outputs: RationaleItem[], note?: string): void;
146
+ }
147
+
148
+ /** The live tracer: a stack of open mechanisms over one {@link Mind.respond}.
149
+ *
150
+ * Sema's inference is single-threaded and strictly sequential — every async
151
+ * step is awaited before the next begins, and `respond` holds no two thoughts
152
+ * at once — so a plain stack exactly tracks the current nesting: {@link enter}
153
+ * pushes, {@link Scope.done} pops, and {@link step} (a mechanism with no
154
+ * sub-steps) is the two fused. The tracer never branches the control flow; it
155
+ * only records it. */
156
+ export class Rationale {
157
+ private next = 0;
158
+ /** Open mechanisms, outermost first. Each frame remembers the last child it
159
+ * has spawned so the next sibling can default its data-flow edge to it. */
160
+ private readonly stack: Array<{
161
+ index: number;
162
+ name: string;
163
+ lastChild: number | null;
164
+ }> = [];
165
+ /** The most recent step index emitted under each mechanism name — the handle
166
+ * a later step uses to name an EARLIER mechanism as its data-flow producer
167
+ * (e.g. cover depends on the latest recognise / computeExtensions). One tracer is
168
+ * built per response and inference is sequential, so "most recent" is exactly
169
+ * "the one that produced the inputs I am about to consume". */
170
+ private readonly lastByName = new Map<string, number>();
171
+
172
+ constructor(private readonly sink: InspectRationale) {}
173
+
174
+ /** The index of the most recent step with this mechanism name, or undefined if
175
+ * none has run. Used to wire an explicit producer edge into {@link
176
+ * Scope.done} / {@link step}'s `deps`. */
177
+ lastIndex(name: string): number | undefined {
178
+ return this.lastByName.get(name);
179
+ }
180
+
181
+ /** The mechanism names currently open, outermost → innermost. */
182
+ private path(leaf: string): string[] {
183
+ const p = this.stack.map((f) => f.name);
184
+ p.push(leaf);
185
+ return p;
186
+ }
187
+
188
+ /** The default data-flow edge for a step entering now: the previous sibling
189
+ * inside the current mechanism, else the enclosing mechanism, else nothing
190
+ * (the root). An explicit `deps` overrides this. */
191
+ private defaultDeps(): number[] {
192
+ const top = this.stack[this.stack.length - 1];
193
+ if (!top) return [];
194
+ return [top.lastChild ?? top.index];
195
+ }
196
+
197
+ /** Reserve this step's index and register it as the current mechanism's most
198
+ * recent child (so the NEXT sibling chains to it) and as the most recent step
199
+ * of its own NAME (so a later mechanism can name it as a producer). */
200
+ private reserve(name: string): number {
201
+ const index = this.next++;
202
+ const top = this.stack[this.stack.length - 1];
203
+ if (top) top.lastChild = index;
204
+ this.lastByName.set(name, index);
205
+ return index;
206
+ }
207
+
208
+ private emit(
209
+ index: number,
210
+ mechanism: string[],
211
+ inputs: RationaleItem[],
212
+ outputs: RationaleItem[],
213
+ deps: number[] | undefined,
214
+ note: string | undefined,
215
+ ): void {
216
+ this.sink({
217
+ index,
218
+ mechanism,
219
+ parent: this.stack.length > 0
220
+ ? this.stack[this.stack.length - 1].index
221
+ : -1,
222
+ dependsOn: deps ?? this.defaultDeps(),
223
+ inputs,
224
+ outputs,
225
+ note,
226
+ });
227
+ }
228
+
229
+ /** Enter a mechanism that has sub-steps. Captures its inputs and the nesting
230
+ * now; the matching {@link Scope.done} supplies the outputs when it finishes.
231
+ * `deps` overrides the default data-flow edge (previous sibling / parent). */
232
+ enter(
233
+ name: string,
234
+ inputs: RationaleItem[],
235
+ deps?: number[],
236
+ ): Scope {
237
+ const mechanism = this.path(name);
238
+ const resolvedDeps = deps ?? this.defaultDeps();
239
+ const index = this.reserve(name);
240
+ this.stack.push({ index, name, lastChild: null });
241
+ let closed = false;
242
+ const emit = this.emit.bind(this);
243
+ const pop = () => {
244
+ // Pop down to and including this frame — tolerant of a sub-mechanism that
245
+ // forgot to close, so one missed `done` cannot desync the whole stack.
246
+ const at = this.stack.findIndex((f) => f.index === index);
247
+ if (at >= 0) this.stack.length = at;
248
+ };
249
+ return {
250
+ index,
251
+ done: (outputs, note) => {
252
+ if (closed) return;
253
+ closed = true;
254
+ pop();
255
+ emit(index, mechanism, inputs, outputs, resolvedDeps, note);
256
+ },
257
+ };
258
+ }
259
+
260
+ /** Record a mechanism that has no sub-steps — its inputs and outputs are both
261
+ * known at the call site. Returns its index, for a later step to depend on. */
262
+ step(
263
+ name: string,
264
+ inputs: RationaleItem[],
265
+ outputs: RationaleItem[],
266
+ note?: string,
267
+ deps?: number[],
268
+ ): number {
269
+ const mechanism = this.path(name);
270
+ const resolvedDeps = deps ?? this.defaultDeps();
271
+ const index = this.reserve(name);
272
+ this.emit(index, mechanism, inputs, outputs, resolvedDeps, note);
273
+ return index;
274
+ }
275
+ }
@@ -0,0 +1,198 @@
1
+ // reasoning.ts — multi-hop reasoning + multi-topic fusion (Section 4 of the mind).
2
+ //
3
+ // reason — extend an answer forward across facts (multi-hop)
4
+ // fuseAttention — fuse independent points of attention (multi-topic)
5
+ import { rItem, rNode } from "./trace.js";
6
+
7
+ import { bytesEqual, indexOf } from "../bytes.js";
8
+ import type { MindContext } from "./types.js";
9
+ import { resolve } from "./primitives.js";
10
+ import { corpusN } from "./traverse.js";
11
+ import { follow, haloSiblings, project } from "./match.js";
12
+ import { joinWithBridge, pivotInto } from "./resonance.js";
13
+ import type { Precomputed } from "./pipeline-mechanism.js";
14
+ import type { Rationale } from "./rationale.js";
15
+
16
+ /** Extend a grounded answer forward across facts (multi-hop reasoning).
17
+ * Pivots on the longest unconsumed learnt context each answer contains,
18
+ * then follows the pivot's continuation to the next fact. Repeats up
19
+ * to `cfg.recallQueryK` hops. `preConsumed` carries node ids already
20
+ * spoken for by the grounding stage (cover/extract/CAST). `pre` is the
21
+ * response's shared pre-computation — the post-grounding stages read the
22
+ * same container the mechanisms did. */
23
+ export async function reason(
24
+ ctx: MindContext,
25
+ query: Uint8Array,
26
+ answer: Uint8Array,
27
+ preConsumed: ReadonlySet<number>,
28
+ pre: Precomputed,
29
+ ): Promise<Uint8Array> {
30
+ // Echo guard: a query that is ITSELF a learnt continuation (some context's
31
+ // answer) is being asked back at the system — hopping forward from it would
32
+ // chain through the very fact that produced it and echo the conversation
33
+ // back. The grounded answer alone is the honest read-out. Deliberately a
34
+ // broad structural gate; pinned by test/31-audit.
35
+ const qId = pre.queryResolved;
36
+ if (qId !== null && ctx.store.prevCount(qId) > 0) return answer;
37
+
38
+ const consumed = new Set<number>();
39
+ // Consume a node and its neighbours for pivot-cycle prevention — CAPPED at
40
+ // the hub bound, via the store's LIMITed edge reads: a common continuation's
41
+ // reverse fan-in (and a hub context's forward fan-out) is corpus-sized, and
42
+ // no per-hop operation may grow with the corpus. The cap follows the one
43
+ // convention every fan-out decision uses (first √N in the relation's own
44
+ // read order); a pivot suppressed only by a beyond-cap neighbour may now
45
+ // fire — the same visibility trade chooseNext documents.
46
+ const hubBound = Math.ceil(Math.sqrt(corpusN(ctx)));
47
+ const consumeNode = (id: number | null) => {
48
+ if (id === null) return;
49
+ consumed.add(id);
50
+ for (const p of ctx.store.prevFirst(id, hubBound)) consumed.add(p);
51
+ };
52
+ const consumeAll = (id: number | null) => {
53
+ if (id === null) return;
54
+ consumeNode(id);
55
+ for (const n of ctx.store.nextFirst(id, hubBound)) consumed.add(n);
56
+ };
57
+
58
+ // Pre-consume whatever the grounding stage already spoke for. The halo
59
+ // sweep is one ANN query per node — cap it at haloQueryK sweeps (cover
60
+ // grounding can pre-consume one node per recognised site, O(query length));
61
+ // nodes past the cap are still consumed directly, they just skip the
62
+ // synonym expansion.
63
+ let haloSweeps = 0;
64
+ for (const id of preConsumed) {
65
+ consumeNode(id);
66
+ if (haloSweeps >= ctx.cfg.haloQueryK) continue;
67
+ const h = ctx.store.halo(id);
68
+ if (!h) continue;
69
+ haloSweeps++;
70
+ for (const sib of await haloSiblings(ctx, id, h)) consumeNode(sib.id);
71
+ }
72
+
73
+ let cur = answer;
74
+ const qv = pre.guide; // the response-wide guide IS the query's gist
75
+ let t: ReturnType<Rationale["enter"]> | undefined;
76
+ const startedFrom = answer;
77
+ for (let hop = 0; hop < ctx.cfg.recallQueryK; hop++) {
78
+ const curId = resolve(ctx, cur);
79
+ consumeNode(curId);
80
+
81
+ // Forward-absorb: follow only UNCONSUMED continuations. The gate below
82
+ // checks an unconsumed edge EXISTS, but follow()'s chooseNext knows
83
+ // nothing of `consumed` and may still walk to a consumed fixpoint —
84
+ // absorbing it would repeat content the grounding stage already spoke
85
+ // for, so a consumed fixpoint falls through to the pivot step instead.
86
+ if (
87
+ curId !== null &&
88
+ ctx.store.nextFirst(curId, hubBound).some((n) => !consumed.has(n))
89
+ ) {
90
+ const fwd = await follow(ctx, curId, qv);
91
+ const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
92
+ if (
93
+ fwd !== null && !bytesEqual(fwd, cur) &&
94
+ (fwdId === null || !consumed.has(fwdId))
95
+ ) {
96
+ consumeAll(curId);
97
+ t ??= ctx.trace?.enter("reason", [
98
+ rItem(startedFrom, "grounded"),
99
+ ]);
100
+ ctx.trace?.step(
101
+ "absorbForward",
102
+ [rItem(cur, "answer", curId)],
103
+ [rItem(fwd, "answer", resolve(ctx, fwd) ?? undefined)],
104
+ "the answer is itself a learnt fact — follow its continuation to the fixpoint",
105
+ );
106
+ cur = fwd;
107
+ continue;
108
+ }
109
+ }
110
+
111
+ // Pivot: find the longest unconsumed learnt context the answer contains.
112
+ consumeAll(curId);
113
+ const pivot = await pivotInto(ctx, cur, consumed);
114
+ if (pivot === null) break;
115
+
116
+ const fc = await follow(ctx, pivot, qv);
117
+ consumeAll(pivot);
118
+ if (fc === null || bytesEqual(fc, cur)) break;
119
+ t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
120
+ ctx.trace?.step(
121
+ "pivotStep",
122
+ [rItem(cur, "answer"), rNode(ctx, pivot, "pivot")],
123
+ [rItem(fc, "answer", resolve(ctx, fc) ?? undefined)],
124
+ "pivot on the shared span this answer contains, then step forward across that fact",
125
+ );
126
+ cur = fc;
127
+ }
128
+ t?.done(
129
+ [rItem(cur, "answer", resolve(ctx, cur) ?? undefined)],
130
+ "the multi-hop chain's fixpoint",
131
+ );
132
+ return cur;
133
+ }
134
+
135
+ /** Fuse independent points of attention into one answer (multi-topic).
136
+ * When the consensus climb finds more than one dominant point, each
137
+ * independent point grounds its own answer; they are bridged together
138
+ * by any learnt connector the graph holds between them. */
139
+ export async function fuseAttention(
140
+ ctx: MindContext,
141
+ query: Uint8Array,
142
+ primary: Uint8Array,
143
+ pre: Precomputed,
144
+ ): Promise<Uint8Array> {
145
+ // When the answer is structurally drawn from the query itself
146
+ // (extraction), it already spans all the query's pieces — fusion
147
+ // would only add noise from unrelated stored contexts. The gate is
148
+ // STRICT containment (resolved node in the query's tree, or a contiguous
149
+ // byte run): the old sparse-subsequence test was trivially satisfied by
150
+ // short answers over long queries, silently starving multi-topic queries
151
+ // of fusion.
152
+ if (containsSpan(ctx, query, primary)) return primary;
153
+
154
+ // The committed points of attention ARE the shared climb's roots (same
155
+ // query, same k, same DF mode) — read them from Precomputed instead of
156
+ // re-climbing, so even a traced response pays for the climb once.
157
+ const forest = (await pre.attention()).roots;
158
+ if (forest.length <= 1) return primary;
159
+
160
+ const pieces: Array<{ start: number; bytes: Uint8Array }> = [
161
+ { start: forest[0].start, bytes: primary },
162
+ ];
163
+ const qv = pre.guide; // once, not per root
164
+ const t = ctx.trace?.enter("fuseAttention", [
165
+ rItem(primary, "primary"),
166
+ ...forest.slice(1).map((r) => rNode(ctx, r.anchor, "point", r.vote)),
167
+ ]);
168
+ for (const root of forest.slice(1)) {
169
+ const g = await project(ctx, root.anchor, qv);
170
+ if (g === null || g.length === 0) continue;
171
+ if (pieces.some((p) => indexOf(p.bytes, g, 0) >= 0)) continue;
172
+ pieces.push({ start: root.start, bytes: g });
173
+ }
174
+ if (pieces.length === 1) {
175
+ t?.done(
176
+ [rItem(primary, "answer")],
177
+ "no further independent point grounded",
178
+ );
179
+ return primary;
180
+ }
181
+
182
+ pieces.sort((a, b) => a.start - b.start);
183
+ let out = pieces[0].bytes;
184
+ for (let i = 1; i < pieces.length; i++) {
185
+ // An approximate-resonance miss (or a genuinely unlearnt junction) joins
186
+ // the pieces bare — joinWithBridge surfaces it as a bridgeMiss step.
187
+ out = await joinWithBridge(ctx, out, pieces[i].bytes);
188
+ }
189
+ t?.done(
190
+ [rItem(out, "answer", resolve(ctx, out) ?? undefined)],
191
+ `fused ${pieces.length} independent points of attention into one answer`,
192
+ );
193
+ return out;
194
+ }
195
+
196
+ // (resonance.js is already a static dependency above — `bridge` — so the old
197
+ // dynamic import of pivotInto guarded against a cycle that does not exist.)
198
+ import { containsSpan } from "./mechanisms/extraction.js";