@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,541 @@
1
+ // alu.ts — the assembled ALU: registry + the synchronous recogniser the search
2
+ // consults.
3
+ //
4
+ // `Alu` wires the kernels into one OperationRegistry and exposes exactly what a
5
+ // host needs to fold computation into its lightest-derivation search:
6
+ //
7
+ // • scan(bytes) — find OPERAND spans (numeric literals) and OPERATOR
8
+ // spans (literal symbolic forms) directly in the raw
9
+ // bytes, independent of any host chunking.
10
+ // • recogniseValue — read a byte span into a Value: a scalar, or a LIST (a
11
+ // run of element values joined by a consistent separator
12
+ // — `[1,2,3]`, `1 2 3`, `1, 2, 3`, none privileged).
13
+ // • lookupOperator — the canonical op(s) a literal surface form names.
14
+ // • applyValues — apply an op to operand VALUES, encode the result.
15
+ // • applyBytes — decode operand spans, apply an op, encode the result;
16
+ // the pure computation a graph rule materialises.
17
+ // • grammar — the registry-derived expression grammar (expr.ts),
18
+ // through which infix runs and integrands evaluate by
19
+ // a recursive application of the same kernel.
20
+ //
21
+ // The facade is also the sub-library's ONE doorway for a host: it is built
22
+ // over an {@link "./parser.js".AluHost} port (resonance + geometry — by default
23
+ // a structural stub, so the ALU runs fully decoupled) and exposes
24
+ //
25
+ // • parse(query) — recognise and evaluate every computation a raw byte
26
+ // query invokes (the query parser, parser.ts);
27
+ // • compute(name, …) — apply an operation to operand byte spans, structure
28
+ // recognised and resonance pre-resolved (the
29
+ // n-dimensional entry point).
30
+ //
31
+ // Operations recognised by MEANING — a synonym form ("plus", "increased by"), a
32
+ // glyph, or an operation the bytes do not spell at all (an integral, a limit) —
33
+ // are resolved only through the host port; the kernel itself stays a pure,
34
+ // deterministic machine over literal numerals and registered surface forms.
35
+
36
+ import {
37
+ asReal,
38
+ decimalCodec,
39
+ nd,
40
+ parseValue,
41
+ real,
42
+ type Value,
43
+ type ValueCodec,
44
+ } from "./value.js";
45
+ import {
46
+ type EvalExpr,
47
+ NO_RESONANCE,
48
+ type Operation,
49
+ OperationRegistry,
50
+ type OpRuntime,
51
+ type ResonanceSync,
52
+ } from "./operation.js";
53
+ import { registerLogic } from "./kernel-logic.js";
54
+ import { registerBits } from "./kernel-bits.js";
55
+ import { registerArith } from "./kernel-arith.js";
56
+ import { registerNumeric } from "./kernel-numeric.js";
57
+ import { registerNd } from "./kernel-nd.js";
58
+ import { ExprGrammar } from "./expr.js";
59
+ import type { ConceptAnchor } from "./resonance.js";
60
+ import {
61
+ type AluHost,
62
+ type ComputedSpan,
63
+ QueryParser,
64
+ STRUCTURAL_HOST,
65
+ } from "./parser.js";
66
+ import {
67
+ CLOSE,
68
+ DOT,
69
+ isDigitByte,
70
+ isSepByte,
71
+ matchBracket,
72
+ OPEN,
73
+ trimEnd,
74
+ trimStart,
75
+ } from "./text.js";
76
+ import { latin1 } from "../../bytes.js";
77
+
78
+ /** A numeric literal found in the byte stream, with its parsed value. */
79
+ export interface OperandSpan {
80
+ i: number;
81
+ j: number;
82
+ value: Value;
83
+ }
84
+
85
+ /** A recognised operator span: the byte range and the canonical op it names. */
86
+ export interface OperatorSpan {
87
+ i: number;
88
+ j: number;
89
+ name: string;
90
+ /** Operand count for INFIX use — a fixed arity as registered, or 2 for a
91
+ * variadic op used as a binary infix operator. */
92
+ arity: number;
93
+ }
94
+
95
+ /** Options for assembling an {@link Alu}. */
96
+ export interface AluOptions {
97
+ /** Decimal places a real result is rounded to before encoding (determinism). */
98
+ precision?: number;
99
+ /** Numerical convergence tolerance and iteration ceiling. */
100
+ tol?: number;
101
+ maxIter?: number;
102
+ }
103
+
104
+ export class Alu {
105
+ readonly registry = new OperationRegistry();
106
+ readonly codec: ValueCodec;
107
+ private readonly rt: OpRuntime;
108
+ /** Symbolic operator forms (no ASCII letter/digit), as byte patterns, sorted
109
+ * longest-first so the scanner matches "<=" before "<". */
110
+ private readonly symbolicForms: Array<
111
+ { bytes: Uint8Array; name: string; arity: number }
112
+ >;
113
+ private _grammar?: ExprGrammar;
114
+ private _anchors?: ReadonlyArray<ConceptAnchor>;
115
+ /** How many {@link apply} calls ended in a caught throw this session, and
116
+ * the most recent caught error. A caught throw is USUALLY a routine
117
+ * decline (a symbol fed to arithmetic — the "rule does not fire"
118
+ * contract), but the same catch would also swallow a genuine kernel bug;
119
+ * these two fields make that observable instead of silent. Zero cost when
120
+ * nothing throws. */
121
+ applyCaught = 0;
122
+ lastApplyError: unknown = null;
123
+ private readonly decoder = new TextDecoder();
124
+ /** The query parser, wired to the host port — internal; hosts reach it only
125
+ * through {@link parse} and {@link compute}. */
126
+ private readonly parser: QueryParser;
127
+
128
+ constructor(opts: AluOptions = {}, host: AluHost = STRUCTURAL_HOST) {
129
+ const precision = opts.precision ?? 6;
130
+ this.codec = decimalCodec(precision);
131
+ this.rt = { tol: opts.tol ?? 1e-10, maxIter: opts.maxIter ?? 1000 };
132
+
133
+ registerLogic(this.registry);
134
+ registerBits(this.registry);
135
+ registerArith(this.registry);
136
+ registerNumeric(this.registry);
137
+ registerNd(this.registry);
138
+
139
+ this.symbolicForms = this.buildSymbolicForms();
140
+ this.parser = new QueryParser(this, host);
141
+ }
142
+
143
+ /** Recognise and evaluate every computation `query` invokes — infix
144
+ * arithmetic runs, and operations named literally or by meaning (through the
145
+ * host port). All async resonance is resolved inside, so the caller
146
+ * receives finished spans it can fold synchronously into its search. See
147
+ * {@link "./parser.js".QueryParser.parse}. */
148
+ parse(query: Uint8Array): Promise<ComputedSpan[]> {
149
+ return this.parser.parse(query);
150
+ }
151
+
152
+ /** Apply an operation to operand byte spans — operand STRUCTURE recognised
153
+ * into Values, every reachable symbol's resonance pre-resolved through the
154
+ * host port — and encode the result canonically; null when the computation
155
+ * declines. See {@link "./parser.js".QueryParser.compute}. */
156
+ compute(
157
+ name: string,
158
+ operandBytes: Uint8Array[],
159
+ asSymbol: (idx: number) => boolean = () => false,
160
+ ): Promise<Uint8Array | null> {
161
+ return this.parser.compute(name, operandBytes, asSymbol);
162
+ }
163
+
164
+ /** Infix arity of an op: its fixed arity, or 2 for a variadic op (an infix
165
+ * operator binds two operands; the registry's variadic fold still accepts
166
+ * the pair). */
167
+ private infixArity(op: Operation): number {
168
+ return op.arity === "variadic" ? 2 : op.arity;
169
+ }
170
+
171
+ /** Collect the symbolic surface forms (punctuation, no letters/digits) as
172
+ * UTF-8 byte patterns. Each maps to the binary claimant when one exists, so
173
+ * an infix scan resolves a shared symbol to its two-operand reading. */
174
+ private buildSymbolicForms(): Array<
175
+ { bytes: Uint8Array; name: string; arity: number }
176
+ > {
177
+ const enc = new TextEncoder();
178
+ const isSymbolic = (s: string) => s.length > 0 && !/[A-Za-z0-9]/.test(s);
179
+ const byForm = new Map<string, string[]>();
180
+ for (const { form, name } of this.registry.formEntries()) {
181
+ if (!isSymbolic(form)) continue;
182
+ const a = byForm.get(form);
183
+ if (a) a.push(name);
184
+ else byForm.set(form, [name]);
185
+ }
186
+ const out: Array<{ bytes: Uint8Array; name: string; arity: number }> = [];
187
+ for (const [form, names] of byForm) {
188
+ // Prefer a binary/variadic claimant for infix matching.
189
+ let chosen = names[0];
190
+ let chosenArity = this.infixArity(this.registry.get(chosen)!);
191
+ for (const n of names) {
192
+ const a = this.infixArity(this.registry.get(n)!);
193
+ if (a === 2) {
194
+ chosen = n;
195
+ chosenArity = a;
196
+ break;
197
+ }
198
+ }
199
+ out.push({ bytes: enc.encode(form), name: chosen, arity: chosenArity });
200
+ }
201
+ // Longest first so a longer form wins over a prefix of it ("<=" before "<").
202
+ out.sort((a, b) => b.bytes.length - a.bytes.length);
203
+ return out;
204
+ }
205
+
206
+ /** The canonical op name(s) a literal surface form names (exact lookup). */
207
+ lookupOperator(form: string): readonly string[] {
208
+ return this.registry.lookupForm(form);
209
+ }
210
+
211
+ /** The operation CONCEPT anchors: the (canonical name, form bytes) pairs a
212
+ * span's MEANING is resonated against, so an operation the bytes do not
213
+ * literally spell (an integral, a derivative, a limit, …, in any modality)
214
+ * is recognised by gist nearness to one of these forms. This is what makes
215
+ * operation recognition generic and modality-agnostic rather than a fixed
216
+ * symbol table — and the ALU, not the host, decides which of its surface
217
+ * forms are anchors, using its own machinery:
218
+ *
219
+ * • a form the SCANNER already reads in full (a pure operator symbol like
220
+ * "<=", a numeral like "0") is excluded — the literal path owns it, and
221
+ * its meaning-space image would only add noise;
222
+ * • a form must be COMPOUND (≥ 2 runes): a single atom's gist is a
223
+ * coordinate of the alphabet, not a distributional meaning, so there is
224
+ * nothing for resonance to read.
225
+ *
226
+ * Cached once; the stable array identity lets the host memoise whatever
227
+ * representation (gists, indices) it derives from it. */
228
+ conceptAnchors(): ReadonlyArray<ConceptAnchor> {
229
+ if (this._anchors) return this._anchors;
230
+ const enc = new TextEncoder();
231
+ const out: ConceptAnchor[] = [];
232
+ for (const { form, name } of this.registry.formEntries()) {
233
+ if ([...form].length < 2) continue; // a lone atom carries no gist
234
+ const bytes = enc.encode(form);
235
+ // Fully claimed by the literal scanner → the literal path owns it.
236
+ const { operands, operators } = this.scan(bytes);
237
+ let claimed = 0;
238
+ for (const t of [...operands, ...operators]) claimed += t.j - t.i;
239
+ if (claimed === bytes.length) continue;
240
+ out.push({ name, form: bytes });
241
+ }
242
+ return (this._anchors = out);
243
+ }
244
+
245
+ /** The registry-derived expression grammar, built once: infix binding and
246
+ * prefix/function/constant resolution all read off the registered ops, and
247
+ * every evaluation step routes back through this registry's own arithmetic —
248
+ * recursion all the way down (see expr.ts). */
249
+ get grammar(): ExprGrammar {
250
+ if (!this._grammar) {
251
+ const scalarCtx = this.registry.context(NO_RESONANCE, this.rt);
252
+ this._grammar = new ExprGrammar(
253
+ this.registry,
254
+ (name, args) => asReal(scalarCtx.apply(name, args.map(real))),
255
+ );
256
+ }
257
+ return this._grammar;
258
+ }
259
+
260
+ /** Evaluate an expression's bytes at a variable binding, through this
261
+ * registry's own arithmetic (see {@link grammar}). */
262
+ evalExpression(
263
+ bytes: Uint8Array,
264
+ variable: string,
265
+ at: number,
266
+ ): number | null {
267
+ return this.grammar.eval(this.decoder.decode(bytes), variable, at);
268
+ }
269
+
270
+ /** Whether a name is a registered operation. */
271
+ has(name: string): boolean {
272
+ return this.registry.has(name);
273
+ }
274
+
275
+ /** The infix arity of a registered op (for the host to size a rule), or 0. */
276
+ arityOf(name: string): number {
277
+ const op = this.registry.get(name);
278
+ return op ? this.infixArity(op) : 0;
279
+ }
280
+
281
+ /** Whether an op acts on an EXPRESSION (a function) rather than plain numbers
282
+ * — declared by the op itself at registration (the `expression` trait), so
283
+ * there is no side table of names here. */
284
+ isExpressionOp(name: string): boolean {
285
+ return this.registry.get(name)?.expression === true;
286
+ }
287
+
288
+ /** Scan a byte span for operand (numeric) and operator (symbolic) spans. A
289
+ * pure, deterministic left-to-right lexer: at each position take a numeral if
290
+ * one starts there, else the longest symbolic operator form, else skip one
291
+ * byte. Numerals and operator symbols are disjoint character classes, so the
292
+ * two never overlap. */
293
+ scan(bytes: Uint8Array): {
294
+ operands: OperandSpan[];
295
+ operators: OperatorSpan[];
296
+ } {
297
+ const operands: OperandSpan[] = [];
298
+ const operators: OperatorSpan[] = [];
299
+ const n = bytes.length;
300
+ let i = 0;
301
+ while (i < n) {
302
+ const c = bytes[i];
303
+ // A numeral: a run of digits, with at most one interior decimal point that
304
+ // is itself flanked by digits (so a trailing "." is left to the operators
305
+ // / skip path, not swallowed).
306
+ if (
307
+ isDigitByte(c) || (c === DOT && i + 1 < n && isDigitByte(bytes[i + 1]))
308
+ ) {
309
+ let j = i;
310
+ let seenDot = false;
311
+ while (j < n) {
312
+ if (isDigitByte(bytes[j])) {
313
+ j++;
314
+ } else if (
315
+ bytes[j] === DOT && !seenDot && j + 1 < n &&
316
+ isDigitByte(bytes[j + 1])
317
+ ) {
318
+ seenDot = true;
319
+ j++;
320
+ } else break;
321
+ }
322
+ operands.push({ i, j, value: parseValue(bytes.subarray(i, j)) });
323
+ i = j;
324
+ continue;
325
+ }
326
+ // The longest symbolic operator form starting here.
327
+ const op = this.matchSymbolic(bytes, i);
328
+ if (op) {
329
+ operators.push({
330
+ i,
331
+ j: i + op.bytes.length,
332
+ name: op.name,
333
+ arity: op.arity,
334
+ });
335
+ i += op.bytes.length;
336
+ continue;
337
+ }
338
+ i++;
339
+ }
340
+ return { operands, operators };
341
+ }
342
+
343
+ private matchSymbolic(
344
+ bytes: Uint8Array,
345
+ at: number,
346
+ ): { bytes: Uint8Array; name: string; arity: number } | null {
347
+ for (const f of this.symbolicForms) {
348
+ const len = f.bytes.length;
349
+ if (at + len > bytes.length) continue;
350
+ let ok = true;
351
+ for (let k = 0; k < len; k++) {
352
+ if (bytes[at + k] !== f.bytes[k]) {
353
+ ok = false;
354
+ break;
355
+ }
356
+ }
357
+ if (ok) return f;
358
+ }
359
+ return null;
360
+ }
361
+
362
+ /** Read a byte span into a {@link Value} by recognising its STRUCTURE — the
363
+ * byte⇄Value boundary the host computes across. The kernel itself only knows
364
+ * the irreducible scalar floor (a numeral's digits → a quantity, via {@link
365
+ * parseValue}); recognising whether a span is one quantity or a LIST of them is
366
+ * layered here, over {@link scan}, so no caller re-implements it.
367
+ *
368
+ * A LIST is a run of element values joined by a CONSISTENT separator. No
369
+ * spelling is privileged: two complementary readings cover the forms a list
370
+ * takes —
371
+ *
372
+ * • CONTAINER — an explicit `[ … ]` group delimits a list whose elements may
373
+ * be anything (symbols, nested groups, mixed): split its interior at the TOP
374
+ * level (bracket-depth aware) on separator runs, each element recognised in
375
+ * turn (so nesting and heterogeneity recurse for free). This is also the
376
+ * codec's canonical OUTPUT spelling, so a computed list feeds straight back
377
+ * in as an operand.
378
+ * • SEQUENCE — a bare run of ≥2 numeric operands with a consistent connective
379
+ * between them (`1 2 3`, `1, 2, 3`, `1 and 2 and 3`): the operands {@link
380
+ * scan} finds are the elements, and whatever sits between them — a space, a
381
+ * comma, " and " — is the separator, accepted as long as it is the same
382
+ * throughout and the run has no leftover edges.
383
+ *
384
+ * Anything else is a SCALAR ({@link parseValue}): a numeral, or an opaque
385
+ * symbol of any modality (a learnt form, an operator name a higher-order op
386
+ * will resolve, a single word). */
387
+ recogniseValue(bytes: Uint8Array): Value {
388
+ const s = bytes.subarray(trimStart(bytes), trimEnd(bytes));
389
+ // CONTAINER: a balanced [ … ] group spanning the whole (trimmed) span.
390
+ if (s.length >= 2 && s[0] === OPEN && matchBracket(s, 0) === s.length - 1) {
391
+ return nd(this.recogniseContainer(s.subarray(1, s.length - 1)));
392
+ }
393
+ // SEQUENCE: ≥2 numeric operands with a consistent connective between them.
394
+ const seq = this.recogniseSequence(s);
395
+ if (seq !== null) return nd(seq);
396
+ // SCALAR floor.
397
+ return parseValue(s);
398
+ }
399
+
400
+ /** Split a CONTAINER's interior into its element values — top-level (bracket-
401
+ * depth aware) spans divided by separator RUNS (maximal runs of {@link
402
+ * isSepByte}). The brackets are the explicit delimiter, so the divider's
403
+ * spelling is not privileged (`[1,2,3]`, `[1, 2, 3]`, `[1 2 3]` are the same
404
+ * list). Each element is recognised in turn, so a nested `[ … ]` element
405
+ * recurses and a symbol element stays opaque. An empty interior is `[]`. */
406
+ private recogniseContainer(inner: Uint8Array): Value[] {
407
+ if (trimEnd(inner) <= trimStart(inner)) return []; // empty / all space → []
408
+ const elements: Uint8Array[] = [];
409
+ let depth = 0;
410
+ let start = 0;
411
+ let p = 0;
412
+ while (p < inner.length) {
413
+ const c = inner[p];
414
+ if (c === OPEN) depth++;
415
+ else if (c === CLOSE) depth--;
416
+ if (depth === 0 && isSepByte(c)) {
417
+ let q = p;
418
+ while (q < inner.length && isSepByte(inner[q])) q++;
419
+ elements.push(inner.subarray(start, p));
420
+ start = q;
421
+ p = q;
422
+ continue;
423
+ }
424
+ p++;
425
+ }
426
+ elements.push(inner.subarray(start));
427
+ return elements.map((e) => this.recogniseValue(e));
428
+ }
429
+
430
+ /** Recognise a bare SEQUENCE — ≥2 numeric operands separated by a consistent
431
+ * connective, with no leftover material at the edges — or null when the span
432
+ * is not such a sequence. The operands are exactly the ones {@link scan}
433
+ * finds; the bytes between consecutive operands are the separator, which need
434
+ * only be the SAME throughout (a space, a comma, " and ", …) — its spelling is
435
+ * not constrained, so no separator is privileged. */
436
+ private recogniseSequence(s: Uint8Array): Value[] | null {
437
+ const { operands } = this.scan(s);
438
+ if (operands.length < 2) return null;
439
+ // No leftover before the first operand or after the last (a clean sequence).
440
+ if (trimStart(s) !== operands[0].i) return null;
441
+ if (trimEnd(s) !== operands[operands.length - 1].j) return null;
442
+ let sep: string | null = null;
443
+ for (let k = 0; k + 1 < operands.length; k++) {
444
+ const gap = latin1(s.subarray(operands[k].j, operands[k + 1].i));
445
+ if (gap.length === 0) return null; // operands must be separated
446
+ if (sep === null) sep = gap;
447
+ else if (gap !== sep) return null; // inconsistent → not one sequence
448
+ }
449
+ return operands.map((o) => this.recogniseValue(s.subarray(o.i, o.j)));
450
+ }
451
+
452
+ /** Apply an op to operand values, with the given pre-resolved resonance.
453
+ * Returns the result value, or null if the op is unknown or the computation
454
+ * throws (e.g. a symbol fed to arithmetic) — the caller treats null as "this
455
+ * rule does not fire", never as a wrong answer. */
456
+ apply(
457
+ name: string,
458
+ operands: Value[],
459
+ resonance: ResonanceSync = NO_RESONANCE,
460
+ ): Value | null {
461
+ if (!this.registry.has(name)) return null;
462
+ try {
463
+ const ctx = this.registry.context(
464
+ resonance,
465
+ this.rt,
466
+ this.makeEvalExpr(),
467
+ );
468
+ return ctx.apply(name, operands);
469
+ } catch (err) {
470
+ // The "rule does not fire" contract — but never invisibly: the counter
471
+ // and lastApplyError let a debugger distinguish a routine decline from
472
+ // a kernel bug that would otherwise read as "computation unknown".
473
+ this.applyCaught++;
474
+ this.lastApplyError = err;
475
+ return null;
476
+ }
477
+ }
478
+
479
+ /** The expression evaluator handed to op contexts, bound to the cached {@link
480
+ * grammar}: the numerical layer's integrand is evaluated by a recursive
481
+ * application of the same derived-from-nand arithmetic. */
482
+ private makeEvalExpr(): EvalExpr {
483
+ return (bytes, variable, at) =>
484
+ this.grammar.eval(this.decoder.decode(bytes), variable, at);
485
+ }
486
+
487
+ /** Decode operand byte spans, apply `name`, and encode the result — the pure
488
+ * computation the graph rule materialises into an output span. Returns null
489
+ * on any failure (unknown op, undecodable operand, thrown computation), so a
490
+ * rule that cannot compute simply does not fire. The result bytes are
491
+ * canonical (see {@link decimalCodec}), so two derivations of the same value
492
+ * agree byte-for-byte — required for the search's chart memoization. */
493
+ applyBytes(
494
+ name: string,
495
+ operandBytes: Uint8Array[],
496
+ resonance: ResonanceSync = NO_RESONANCE,
497
+ ): Uint8Array | null {
498
+ return this.applyBytesTyped(name, operandBytes, () => false, resonance);
499
+ }
500
+
501
+ /** Apply `name` to operand VALUES (a scalar, or an `nd` recognised by {@link
502
+ * recogniseValue}), and encode the result canonically. This is the entry
503
+ * point for computation over STRUCTURE: the scalar kernel and the nd kernel
504
+ * both consume Values, so once a span is recognised the computation is the
505
+ * same whether the operand came in as a number or a list. Returns null on any
506
+ * failure (unknown op, thrown computation), the same "this rule does not fire"
507
+ * contract as {@link applyBytes}. */
508
+ applyValues(
509
+ name: string,
510
+ operands: Value[],
511
+ resonance: ResonanceSync = NO_RESONANCE,
512
+ ): Uint8Array | null {
513
+ const result = this.apply(name, operands, resonance);
514
+ if (result === null) return null;
515
+ return this.codec.encode(result);
516
+ }
517
+
518
+ /** Like {@link applyBytes}, but `asSymbol(idx)` marks operand positions that
519
+ * must be kept as an opaque SYMBOL (not parsed as a number) — the convention
520
+ * the numerical layer needs, where operand 0 is an EXPRESSION (a function's
521
+ * bytes) the kernel samples via the expression evaluator, not a numeral. */
522
+ applyBytesTyped(
523
+ name: string,
524
+ operandBytes: Uint8Array[],
525
+ asSymbol: (idx: number) => boolean,
526
+ resonance: ResonanceSync = NO_RESONANCE,
527
+ ): Uint8Array | null {
528
+ const operands: Value[] = [];
529
+ for (let i = 0; i < operandBytes.length; i++) {
530
+ const b = operandBytes[i];
531
+ const v = asSymbol(i)
532
+ ? { domain: "symbol" as const, bytes: b }
533
+ : this.codec.decode(b);
534
+ if (v === null) return null;
535
+ operands.push(v);
536
+ }
537
+ const result = this.apply(name, operands, resonance);
538
+ if (result === null) return null;
539
+ return this.codec.encode(result);
540
+ }
541
+ }