@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.
- package/AGENTS.md +470 -0
- package/AUTHORS.md +12 -0
- package/COMMERCIAL-LICENSE.md +19 -0
- package/CONTRIBUTING.md +15 -0
- package/HOW_IT_WORKS.md +4210 -0
- package/LICENSE.md +117 -0
- package/README.md +290 -0
- package/TRADEMARKS.md +19 -0
- package/example/demo.ts +46 -0
- package/example/train_base.ts +2675 -0
- package/index.html +893 -0
- package/package.json +25 -0
- package/src/alphabet.ts +34 -0
- package/src/alu/README.md +332 -0
- package/src/alu/src/alu.ts +541 -0
- package/src/alu/src/expr.ts +339 -0
- package/src/alu/src/index.ts +115 -0
- package/src/alu/src/kernel-arith.ts +377 -0
- package/src/alu/src/kernel-bits.ts +199 -0
- package/src/alu/src/kernel-logic.ts +102 -0
- package/src/alu/src/kernel-nd.ts +235 -0
- package/src/alu/src/kernel-numeric.ts +466 -0
- package/src/alu/src/operation.ts +344 -0
- package/src/alu/src/parser.ts +574 -0
- package/src/alu/src/resonance.ts +161 -0
- package/src/alu/src/text.ts +83 -0
- package/src/alu/src/value.ts +327 -0
- package/src/alu/test/alu.test.ts +1004 -0
- package/src/bytes.ts +62 -0
- package/src/config.ts +227 -0
- package/src/derive/README.md +295 -0
- package/src/derive/src/deduction.ts +263 -0
- package/src/derive/src/index.ts +24 -0
- package/src/derive/src/priority-queue.ts +70 -0
- package/src/derive/src/rewrite.ts +127 -0
- package/src/derive/src/trie.ts +242 -0
- package/src/derive/test/derive.test.ts +137 -0
- package/src/extension.ts +42 -0
- package/src/geometry.ts +511 -0
- package/src/index.ts +38 -0
- package/src/ingest-cache.ts +224 -0
- package/src/mind/articulation.ts +137 -0
- package/src/mind/attention.ts +1061 -0
- package/src/mind/canonical.ts +107 -0
- package/src/mind/graph-search.ts +1100 -0
- package/src/mind/index.ts +18 -0
- package/src/mind/junction.ts +347 -0
- package/src/mind/learning.ts +246 -0
- package/src/mind/match.ts +507 -0
- package/src/mind/mechanisms/alu.ts +33 -0
- package/src/mind/mechanisms/cast.ts +568 -0
- package/src/mind/mechanisms/confluence.ts +268 -0
- package/src/mind/mechanisms/cover.ts +248 -0
- package/src/mind/mechanisms/extraction.ts +422 -0
- package/src/mind/mechanisms/recall.ts +241 -0
- package/src/mind/mind.ts +483 -0
- package/src/mind/pipeline-mechanism.ts +326 -0
- package/src/mind/pipeline.ts +300 -0
- package/src/mind/primitives.ts +201 -0
- package/src/mind/rationale.ts +275 -0
- package/src/mind/reasoning.ts +198 -0
- package/src/mind/recognition.ts +234 -0
- package/src/mind/resonance.ts +0 -0
- package/src/mind/trace.ts +108 -0
- package/src/mind/traverse.ts +556 -0
- package/src/mind/types.ts +281 -0
- package/src/rabitq-hnsw/README.md +303 -0
- package/src/rabitq-hnsw/src/database.ts +492 -0
- package/src/rabitq-hnsw/src/heap.ts +90 -0
- package/src/rabitq-hnsw/src/hnsw.ts +514 -0
- package/src/rabitq-hnsw/src/index.ts +15 -0
- package/src/rabitq-hnsw/src/prng.ts +39 -0
- package/src/rabitq-hnsw/src/rabitq.ts +308 -0
- package/src/rabitq-hnsw/src/store.ts +994 -0
- package/src/rabitq-hnsw/test/hnsw.test.ts +1213 -0
- package/src/store-sqlite.ts +928 -0
- package/src/store.ts +2148 -0
- package/src/vec.ts +124 -0
- package/test/00-extract.test.mjs +151 -0
- package/test/01-floor.test.mjs +20 -0
- package/test/02-roundtrip.test.mjs +83 -0
- package/test/03-recall.test.mjs +98 -0
- package/test/04-think.test.mjs +627 -0
- package/test/05-concepts.test.mjs +84 -0
- package/test/08-storage.test.mjs +948 -0
- package/test/09-edges.test.mjs +65 -0
- package/test/11-universality.test.mjs +180 -0
- package/test/13-conversation.test.mjs +228 -0
- package/test/14-scaling.test.mjs +503 -0
- package/test/15-decomposition-gap.test.mjs +0 -0
- package/test/16-bridge.test.mjs +250 -0
- package/test/17-intelligence.test.mjs +240 -0
- package/test/18-alu.test.mjs +209 -0
- package/test/19-nd.test.mjs +254 -0
- package/test/20-stability.test.mjs +489 -0
- package/test/21-partial.test.mjs +158 -0
- package/test/22-multihop.test.mjs +130 -0
- package/test/23-rationale.test.mjs +316 -0
- package/test/24-generalization.test.mjs +416 -0
- package/test/25-embedding.test.mjs +75 -0
- package/test/26-cooperative.test.mjs +89 -0
- package/test/27-saturation-drop.test.mjs +637 -0
- package/test/28-unknowable.test.mjs +88 -0
- package/test/29-counterfactual.test.mjs +476 -0
- package/test/30-conflict-resolution.test.mjs +166 -0
- package/test/31-audit.test.mjs +288 -0
- package/test/32-confluence.test.mjs +173 -0
- package/test/33-multi-candidate.test.mjs +222 -0
- package/test/34-cross-region.test.mjs +252 -0
- package/tsconfig.json +16 -0
package/src/bytes.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// bytes.ts — small, pure byte-span utilities.
|
|
2
|
+
//
|
|
3
|
+
// Nothing here knows about Sema, the store, or the search; these are the
|
|
4
|
+
// mechanical operations on Uint8Arrays that the rest of the code leans on, kept
|
|
5
|
+
// together so a reader meets them once and never wonders whether a given helper
|
|
6
|
+
// hides a side effect.
|
|
7
|
+
|
|
8
|
+
/** True when two byte spans are equal in length and content. */
|
|
9
|
+
export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
10
|
+
if (a.length !== b.length) return false;
|
|
11
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Concatenate byte arrays. Takes an array rather than rest params so
|
|
16
|
+
* a large segment list can never overflow the call stack via spread. */
|
|
17
|
+
export function concatBytes(parts: Uint8Array[]): Uint8Array {
|
|
18
|
+
let total = 0;
|
|
19
|
+
for (const p of parts) total += p.length;
|
|
20
|
+
const out = new Uint8Array(total);
|
|
21
|
+
let off = 0;
|
|
22
|
+
for (const p of parts) {
|
|
23
|
+
out.set(p, off);
|
|
24
|
+
off += p.length;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Join two byte spans — the hot two-operand case of {@link concatBytes},
|
|
30
|
+
* fused without the array wrapper for the search's inner fuse loop. */
|
|
31
|
+
export function concat2(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
32
|
+
const out = new Uint8Array(a.length + b.length);
|
|
33
|
+
out.set(a, 0);
|
|
34
|
+
out.set(b, a.length);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Latin-1 view of a byte span — a stable, lossless string key for chart
|
|
39
|
+
* memoization (every byte 0–255 maps to one code unit). */
|
|
40
|
+
export function latin1(b: Uint8Array): string {
|
|
41
|
+
let s = "";
|
|
42
|
+
for (let k = 0; k < b.length; k++) s += String.fromCharCode(b[k]);
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** First index ≥ `from` at which `needle` occurs in `hay`, or -1. A short naive
|
|
47
|
+
* scan — used only to locate a result span inside a learnt framing form. */
|
|
48
|
+
export function indexOf(
|
|
49
|
+
hay: Uint8Array,
|
|
50
|
+
needle: Uint8Array,
|
|
51
|
+
from: number,
|
|
52
|
+
): number {
|
|
53
|
+
if (needle.length === 0) return from;
|
|
54
|
+
outer:
|
|
55
|
+
for (let i = Math.max(0, from); i + needle.length <= hay.length; i++) {
|
|
56
|
+
for (let j = 0; j < needle.length; j++) {
|
|
57
|
+
if (hay[i + j] !== needle[j]) continue outer;
|
|
58
|
+
}
|
|
59
|
+
return i;
|
|
60
|
+
}
|
|
61
|
+
return -1;
|
|
62
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// config.ts — the single configuration interface for Sema.
|
|
2
|
+
// Every tunable parameter lives here. Subsystems receive their subset.
|
|
3
|
+
|
|
4
|
+
// ── Sub-interfaces (grouped by subsystem) ──
|
|
5
|
+
|
|
6
|
+
// NOTE: there is deliberately no resonance-threshold config. The concept
|
|
7
|
+
// threshold is DERIVED from the geometry (see conceptThreshold(D) in
|
|
8
|
+
// geometry.ts) and every consumer reads it from there; a config knob for it
|
|
9
|
+
// existed once but was never read — a dead setting that silently did nothing.
|
|
10
|
+
|
|
11
|
+
export interface AluConfig {
|
|
12
|
+
/** Whether the ALU sub-lib contributes computation rules to the graph search.
|
|
13
|
+
* When false, no operator/operand pre-resolution runs and no ALU rule fires —
|
|
14
|
+
* thinking behaves exactly as it did before ALU existed. */
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
/** Convergence tolerance ε for the numerical limit layer (diff/solve/exp/…). */
|
|
17
|
+
tol: number;
|
|
18
|
+
/** Hard iteration ceiling for any convergence loop, so a non-converging
|
|
19
|
+
* refinement still terminates. */
|
|
20
|
+
maxIter: number;
|
|
21
|
+
/** Decimal places a real result is rounded to before it is encoded to bytes.
|
|
22
|
+
* Determinism here is load-bearing: the search keys an output span by its
|
|
23
|
+
* bytes, so two derivations of the same value must spell it identically. */
|
|
24
|
+
precision: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GeometryConfig {
|
|
28
|
+
/** Maximum siblings per tree fold. */
|
|
29
|
+
maxGroup: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AlphabetConfig {
|
|
33
|
+
/** How different neighbouring byte vectors are from their coarse ancestors
|
|
34
|
+
* (0 = identical, 1 = independent). */
|
|
35
|
+
roughness: number;
|
|
36
|
+
/** Seed XOR mask for the alphabet's PRNG derivation. */
|
|
37
|
+
seedMask: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface StoreConfig {
|
|
41
|
+
minHaloMass: number;
|
|
42
|
+
m: number;
|
|
43
|
+
efConstruction: number;
|
|
44
|
+
/** Construction budget for REACH-ONLY interior gists — the ~90% of content
|
|
45
|
+
* index inserts that exist so a partial query can resonate a sub-region
|
|
46
|
+
* and climb, never as dedup targets. Deposit roots and halo-bearing
|
|
47
|
+
* targets always build with the full `efConstruction`. A smaller budget
|
|
48
|
+
* here is the one deliberate speed-for-quality trade in ingestion (an
|
|
49
|
+
* interior's layer-0 wiring is built from a narrower candidate beam);
|
|
50
|
+
* the recall suite (partial recall, multi-topic attention, counterfactual
|
|
51
|
+
* anchoring) is the gate for its value. Set equal to `efConstruction`
|
|
52
|
+
* to disable the trade. */
|
|
53
|
+
efConstructionInterior: number;
|
|
54
|
+
efSearch: number;
|
|
55
|
+
/** Compact the in-memory vector indices after this many vectors are written.
|
|
56
|
+
* Compaction rebuilds an index from its live codes to reclaim the slots left
|
|
57
|
+
* by tombstoned (updated/deleted) halo entries; pacing it on write VOLUME
|
|
58
|
+
* (not on a flush count that goes quiet during repeat-heavy training) keeps
|
|
59
|
+
* the index dense and query cost bounded. */
|
|
60
|
+
compactEveryNWrites: number;
|
|
61
|
+
/** Over-fetch factor for HNSW queries (ANN recall cushion). */
|
|
62
|
+
overfetch: number;
|
|
63
|
+
/** Combined buffered-write ceiling before a flush of both vector indices
|
|
64
|
+
* (content + halo). Higher ⇒ fewer, larger flushes into the in-memory
|
|
65
|
+
* indices and fewer write-transaction commits. */
|
|
66
|
+
batchSize: number;
|
|
67
|
+
/** Max entries in the store's exact-content dedup map (bounds RAM on huge
|
|
68
|
+
* corpora; a miss only risks a duplicate node, never incorrectness). */
|
|
69
|
+
dedupCacheMax: number;
|
|
70
|
+
/** Max bytes of reconstructed content cached in memory (regenerable).
|
|
71
|
+
* Large branch nodes cost more budget than small leaves, so the cache
|
|
72
|
+
* naturally favours cheap, frequently-hit entries. */
|
|
73
|
+
bytesCacheMax: number;
|
|
74
|
+
/** Max bytes of node-record cache (avoids repeated SQLite lookups for
|
|
75
|
+
* shared DAG nodes). Each record is ~30-50 bytes. */
|
|
76
|
+
recCacheBytes: number;
|
|
77
|
+
/** Max bytes of ingest-result cache used by {@link CachedIngest}. */
|
|
78
|
+
ingestCacheBytes: number;
|
|
79
|
+
/** Max bytes of captured-but-not-yet-indexed node gists (D·4 each). A node's
|
|
80
|
+
* gist enters the content index lazily, only when it first becomes a
|
|
81
|
+
* resonance target (gains a continuation edge or a halo); until then its gist
|
|
82
|
+
* waits here. A deposit links/pours a node right after interning it, so the
|
|
83
|
+
* working set is one deposit's nodes — a modest budget captures ~all of it.
|
|
84
|
+
* An eviction only means a node is reached by the structural DAG climb instead
|
|
85
|
+
* of by direct resonance — a little recall reach, never correctness. */
|
|
86
|
+
pendingGistBytes: number;
|
|
87
|
+
/** Max bytes of EXACT halo accumulators kept in memory (D·4 each). The
|
|
88
|
+
* durable halo row is 2-bit quantized; this cache keeps the accumulators a
|
|
89
|
+
* session is actively pouring into at full precision, so within-session
|
|
90
|
+
* accumulate-then-compare (concept formation as it happens) never
|
|
91
|
+
* round-trips through the quantizer. An eviction or a reopen reads the
|
|
92
|
+
* 2-bit row — the fidelity every cross-session consumer already gets. */
|
|
93
|
+
haloCacheBytes: number;
|
|
94
|
+
/** Size, in MiB, of each `rabitq-hnsw` `VectorDatabase`'s memory budget
|
|
95
|
+
* (forwarded as its `cacheSizeMb`). It is the index's SINGLE memory knob: it
|
|
96
|
+
* sizes both SQLite's page cache and the derived immutable-code LRU. A PURE
|
|
97
|
+
* latency optimisation — the index reads codes from SQLite on demand, so its
|
|
98
|
+
* correctness and its asymptotic per-operation storage-read count are identical
|
|
99
|
+
* with the budget at 0. Exposed so a scaling test can set it to 0 and measure
|
|
100
|
+
* the honest, cache-independent cost. */
|
|
101
|
+
vectorCacheMb: number;
|
|
102
|
+
/** Max entries in the skipped-interior LRU set. Interiors that
|
|
103
|
+
* {@link Store.indexSubtree} has already visited (indexed or skipped) are
|
|
104
|
+
* remembered here so subsequent calls prune their subtrees. Session-local
|
|
105
|
+
* (regenerable). */
|
|
106
|
+
coveredIdsMax: number;
|
|
107
|
+
/** Max bytes of transparent-chain runs ({@link Store.chainRun}) cached for
|
|
108
|
+
* the store's lifetime (~4 bytes per chain node). Valid until any write
|
|
109
|
+
* could break a node's transparency (a new structural parent or a new
|
|
110
|
+
* continuation edge), when the whole cache is dropped — writes happen in
|
|
111
|
+
* training bursts, reads in read-only query phases, so the cache pays for
|
|
112
|
+
* itself exactly where it matters. Regenerable; a miss re-walks. */
|
|
113
|
+
chainCacheBytes: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── The one configuration interface ──
|
|
117
|
+
|
|
118
|
+
export interface MindConfig {
|
|
119
|
+
seed: number;
|
|
120
|
+
recallQueryK: number;
|
|
121
|
+
haloQueryK: number;
|
|
122
|
+
normalizeEpsilon: number;
|
|
123
|
+
cosineEpsilon: number;
|
|
124
|
+
|
|
125
|
+
alu: AluConfig;
|
|
126
|
+
geometry: GeometryConfig;
|
|
127
|
+
alphabet: AlphabetConfig;
|
|
128
|
+
store: StoreConfig;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Defaults ──
|
|
132
|
+
|
|
133
|
+
export const DEFAULT_CONFIG: MindConfig = {
|
|
134
|
+
seed: 42,
|
|
135
|
+
recallQueryK: 12,
|
|
136
|
+
haloQueryK: 12,
|
|
137
|
+
normalizeEpsilon: 1e-12,
|
|
138
|
+
cosineEpsilon: 1e-12,
|
|
139
|
+
alu: {
|
|
140
|
+
enabled: true,
|
|
141
|
+
tol: 1e-10,
|
|
142
|
+
maxIter: 1000,
|
|
143
|
+
precision: 6,
|
|
144
|
+
},
|
|
145
|
+
geometry: {
|
|
146
|
+
maxGroup: 4,
|
|
147
|
+
},
|
|
148
|
+
alphabet: {
|
|
149
|
+
roughness: 0.65,
|
|
150
|
+
seedMask: 0xa1fa17,
|
|
151
|
+
},
|
|
152
|
+
store: {
|
|
153
|
+
minHaloMass: 1,
|
|
154
|
+
m: 8,
|
|
155
|
+
efConstruction: 64,
|
|
156
|
+
efConstructionInterior: 16,
|
|
157
|
+
efSearch: 64,
|
|
158
|
+
compactEveryNWrites: 50_000,
|
|
159
|
+
overfetch: 4,
|
|
160
|
+
batchSize: 256,
|
|
161
|
+
dedupCacheMax: 1_000_000,
|
|
162
|
+
bytesCacheMax: 20_000_000,
|
|
163
|
+
recCacheBytes: 10_000_000,
|
|
164
|
+
ingestCacheBytes: 50_000_000,
|
|
165
|
+
pendingGistBytes: 16_000_000,
|
|
166
|
+
haloCacheBytes: 16_000_000,
|
|
167
|
+
vectorCacheMb: 64,
|
|
168
|
+
coveredIdsMax: 100_000,
|
|
169
|
+
chainCacheBytes: 16_000_000,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// ── Config resolver: partial input + defaults = full config ──
|
|
174
|
+
|
|
175
|
+
export function resolveConfig(opts: Partial<MindConfig> = {}): MindConfig {
|
|
176
|
+
return {
|
|
177
|
+
seed: opts.seed ?? DEFAULT_CONFIG.seed,
|
|
178
|
+
recallQueryK: opts.recallQueryK ?? DEFAULT_CONFIG.recallQueryK,
|
|
179
|
+
haloQueryK: opts.haloQueryK ?? DEFAULT_CONFIG.haloQueryK,
|
|
180
|
+
normalizeEpsilon: opts.normalizeEpsilon ?? DEFAULT_CONFIG.normalizeEpsilon,
|
|
181
|
+
cosineEpsilon: opts.cosineEpsilon ?? DEFAULT_CONFIG.cosineEpsilon,
|
|
182
|
+
alu: {
|
|
183
|
+
enabled: opts.alu?.enabled ?? DEFAULT_CONFIG.alu.enabled,
|
|
184
|
+
tol: opts.alu?.tol ?? DEFAULT_CONFIG.alu.tol,
|
|
185
|
+
maxIter: opts.alu?.maxIter ?? DEFAULT_CONFIG.alu.maxIter,
|
|
186
|
+
precision: opts.alu?.precision ?? DEFAULT_CONFIG.alu.precision,
|
|
187
|
+
},
|
|
188
|
+
geometry: {
|
|
189
|
+
maxGroup: opts.geometry?.maxGroup ?? DEFAULT_CONFIG.geometry.maxGroup,
|
|
190
|
+
},
|
|
191
|
+
alphabet: {
|
|
192
|
+
roughness: opts.alphabet?.roughness ?? DEFAULT_CONFIG.alphabet.roughness,
|
|
193
|
+
seedMask: opts.alphabet?.seedMask ?? DEFAULT_CONFIG.alphabet.seedMask,
|
|
194
|
+
},
|
|
195
|
+
store: {
|
|
196
|
+
minHaloMass: opts.store?.minHaloMass ?? DEFAULT_CONFIG.store.minHaloMass,
|
|
197
|
+
m: opts.store?.m ?? DEFAULT_CONFIG.store.m,
|
|
198
|
+
efConstruction: opts.store?.efConstruction ??
|
|
199
|
+
DEFAULT_CONFIG.store.efConstruction,
|
|
200
|
+
efConstructionInterior: opts.store?.efConstructionInterior ??
|
|
201
|
+
DEFAULT_CONFIG.store.efConstructionInterior,
|
|
202
|
+
efSearch: opts.store?.efSearch ?? DEFAULT_CONFIG.store.efSearch,
|
|
203
|
+
compactEveryNWrites: opts.store?.compactEveryNWrites ??
|
|
204
|
+
DEFAULT_CONFIG.store.compactEveryNWrites,
|
|
205
|
+
overfetch: opts.store?.overfetch ?? DEFAULT_CONFIG.store.overfetch,
|
|
206
|
+
batchSize: opts.store?.batchSize ?? DEFAULT_CONFIG.store.batchSize,
|
|
207
|
+
dedupCacheMax: opts.store?.dedupCacheMax ??
|
|
208
|
+
DEFAULT_CONFIG.store.dedupCacheMax,
|
|
209
|
+
bytesCacheMax: opts.store?.bytesCacheMax ??
|
|
210
|
+
DEFAULT_CONFIG.store.bytesCacheMax,
|
|
211
|
+
recCacheBytes: opts.store?.recCacheBytes ??
|
|
212
|
+
DEFAULT_CONFIG.store.recCacheBytes,
|
|
213
|
+
ingestCacheBytes: opts.store?.ingestCacheBytes ??
|
|
214
|
+
DEFAULT_CONFIG.store.ingestCacheBytes,
|
|
215
|
+
pendingGistBytes: opts.store?.pendingGistBytes ??
|
|
216
|
+
DEFAULT_CONFIG.store.pendingGistBytes,
|
|
217
|
+
haloCacheBytes: opts.store?.haloCacheBytes ??
|
|
218
|
+
DEFAULT_CONFIG.store.haloCacheBytes,
|
|
219
|
+
vectorCacheMb: opts.store?.vectorCacheMb ??
|
|
220
|
+
DEFAULT_CONFIG.store.vectorCacheMb,
|
|
221
|
+
coveredIdsMax: opts.store?.coveredIdsMax ??
|
|
222
|
+
DEFAULT_CONFIG.store.coveredIdsMax,
|
|
223
|
+
chainCacheBytes: opts.store?.chainCacheBytes ??
|
|
224
|
+
DEFAULT_CONFIG.store.chainCacheBytes,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# derive
|
|
2
|
+
|
|
3
|
+
A small, dependency-free library for computing the **lightest derivation** in a
|
|
4
|
+
weighted deduction system — equivalently, an implicit **AND/OR hypergraph** — by
|
|
5
|
+
A\* search. It is the generic graph-exploration core of symbolic processing: the
|
|
6
|
+
engine knows nothing about what its items _are_, only how to canonicalise them,
|
|
7
|
+
enumerate their rules, score them, and recognise the goal. Anything that can be
|
|
8
|
+
phrased as "derive a global structure from weighted rules" — parsing, shortest
|
|
9
|
+
paths, segmentation, rewriting, planning — is one call to the same search.
|
|
10
|
+
|
|
11
|
+
It has no dependency on the rest of the codebase and is intended to be reused
|
|
12
|
+
across mechanisms, in the spirit of a self-contained sublibrary.
|
|
13
|
+
|
|
14
|
+
## The algorithm
|
|
15
|
+
|
|
16
|
+
The library implements **adapted A\* Lightest Derivation (adapted A\*LD)**:
|
|
17
|
+
|
|
18
|
+
> P. F. Felzenszwalb and D. McAllester. _The Generalized A\* Architecture._
|
|
19
|
+
> Journal of Artificial Intelligence Research 29 (2007) 153–190.
|
|
20
|
+
|
|
21
|
+
adapted A\*LD generalises A\* from shortest paths to the problem of computing a
|
|
22
|
+
lightest derivation of a goal from a set of weighted rules, searching an AND/OR
|
|
23
|
+
graph bottom-up. It rests on two classical results and unifies them:
|
|
24
|
+
|
|
25
|
+
- **Knuth (1977)**, _A generalization of Dijkstra's algorithm_ — the
|
|
26
|
+
lightest-derivation problem and its Dijkstra-like solution: process items in
|
|
27
|
+
priority order, and an item's cost is final the instant it is removed from the
|
|
28
|
+
agenda.
|
|
29
|
+
- **A\* parsing** (Klein & Manning, 2003) — adding an admissible heuristic so
|
|
30
|
+
that partial derivations which cannot lead cheaply to the goal are never
|
|
31
|
+
expanded.
|
|
32
|
+
|
|
33
|
+
For a problem that happens to be a shortest path, adapted A\*LD reduces exactly
|
|
34
|
+
to A\*. With a small number of antecedents per rule it runs in **O(M log N)** (M
|
|
35
|
+
rules, N items), and — crucially — it is **output-sensitive**: only items `B`
|
|
36
|
+
with `ℓ(B) ≤ ℓ(goal)` are ever expanded, where `ℓ(B)` is the cost of `B`'s
|
|
37
|
+
lightest derivation. Work is proportional to the goal, not to the size of the
|
|
38
|
+
implicit graph, so there is no need for length caps or other artificial bounds
|
|
39
|
+
to keep it tractable.
|
|
40
|
+
|
|
41
|
+
> **Evidence pooling via semiring generalization.** The `derive` engine is no
|
|
42
|
+
> the pure classic A\*LD algorithm. It has been extended with **additive
|
|
43
|
+
> evidence accumulation** — a second combining mode (`combine: "sum"`) that
|
|
44
|
+
> operates in the **arithmetic (+, +) semiring** instead of the standard
|
|
45
|
+
> **tropical (min, +) semiring** of shortest-path search. Under the min-cost
|
|
46
|
+
> regime, only the single cheapest route to a conclusion survives; under the
|
|
47
|
+
> arithmetic semiring, every independent line of evidence corroborating the same
|
|
48
|
+
> conclusion is **pooled** — its cost is summed, its contribution is recorded,
|
|
49
|
+
> and nothing is discarded for not being the cheapest. This is how Sema forms
|
|
50
|
+
> consensus votes from multiple independent query regions: each region
|
|
51
|
+
> contributes its evidence, and the pooled aggregate is the conclusion's total
|
|
52
|
+
> evidential support. A pooled conclusion never competes in the min-cost chart,
|
|
53
|
+
> never re-enters the agenda, and is read back by the caller once the search
|
|
54
|
+
> exhausts its axioms — making evidence pooling a zero-cost opt-in that coexists
|
|
55
|
+
> with the classic algorithm in a single search.
|
|
56
|
+
|
|
57
|
+
## What "lightest derivation" means
|
|
58
|
+
|
|
59
|
+
A **weighted deduction system** is a set of _items_ combined by inference rules
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
premise₁ ∧ … ∧ premiseₖ --localCost--> conclusion
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A _derivation_ of an item is a tree: the root is a rule, its children are
|
|
66
|
+
derivations of that rule's premises, and the leaves are axioms. A derivation's
|
|
67
|
+
cost is the sum of the local costs of the rules it uses. `ℓ(B)` is the minimum
|
|
68
|
+
such cost over all derivations of `B`. The search returns a derivation achieving
|
|
69
|
+
`ℓ(goal)`, or `null` if the goal is underivable.
|
|
70
|
+
|
|
71
|
+
A rule with one premise is an ordinary (OR) edge; a rule with several premises
|
|
72
|
+
is an **AND** node that _composes_ its premises — this is what makes the
|
|
73
|
+
structure a hypergraph rather than a plain graph, and it is how partial results
|
|
74
|
+
are joined (for example, combining two recognised spans into one).
|
|
75
|
+
|
|
76
|
+
### Bridges: composing premises into a coherent whole
|
|
77
|
+
|
|
78
|
+
A multi-premise rule is a **bridge**: it derives a conclusion from two (or more)
|
|
79
|
+
already-derived items and charges a local cost for the join. Bridges are the
|
|
80
|
+
mechanism by which independently-derived parts are assembled — and because a
|
|
81
|
+
bridge's conclusion is itself an ordinary item, it can serve as a premise of a
|
|
82
|
+
_further_ bridge, so bridges chain associatively: `A∧B → AB`, then `AB∧C → ABC`,
|
|
83
|
+
all within one search. The cost of the join is paid inside the derivation, so
|
|
84
|
+
the lightest derivation is the globally most coherent assembly of the parts, not
|
|
85
|
+
a left-to-right stitch decided after the fact.
|
|
86
|
+
|
|
87
|
+
This is what backs the connector ("bridge") mechanism in the surrounding mind:
|
|
88
|
+
two rewritten spans are joined by a bridge whose local cost reflects how well a
|
|
89
|
+
learned connector fits between them, and a third span chains onto the result by
|
|
90
|
+
the same rule — so a multi-part answer is found as one whole, with no
|
|
91
|
+
accumulated gaps, and never as a post-processing pass.
|
|
92
|
+
|
|
93
|
+
Two properties make bridges safe to lean on:
|
|
94
|
+
|
|
95
|
+
- **Lazy, order-free firing.** A bridge is emitted from `rules` when _either_
|
|
96
|
+
premise is finalised; the engine fires it only once _all_ its premises are
|
|
97
|
+
known (see `lightestDerivation`'s readiness check). So a bridge may be yielded
|
|
98
|
+
twice — once from each side — and the engine deduplicates by waiting for the
|
|
99
|
+
full conjunction. The caller need not know which premise finalises first.
|
|
100
|
+
- **Robust reconstruction.** Recovering the derivation tree is iterative (an
|
|
101
|
+
explicit post-order stack), so it handles both arbitrarily long single-premise
|
|
102
|
+
chains — which, on long inputs, would overflow a recursive walk — and
|
|
103
|
+
multi-premise bridges uniformly. A bridged conclusion's `premises` array holds
|
|
104
|
+
one `Derivation` per premise, in rule order.
|
|
105
|
+
|
|
106
|
+
## The four mechanisms
|
|
107
|
+
|
|
108
|
+
The search stays proportional to the goal because of four reductions, three of
|
|
109
|
+
which the caller participates in through the `DeductionSystem` interface:
|
|
110
|
+
|
|
111
|
+
1. **Canonical chart memoization** — items are keyed by `key(item)`; equivalent
|
|
112
|
+
partial derivations collapse to a single chart entry, the cheapest one.
|
|
113
|
+
2. **Backward demand filtering** — `rules` only emits rules whose conclusion can
|
|
114
|
+
still reach the goal, so work unrelated to the goal is never generated.
|
|
115
|
+
3. **A\* lower-bound pruning** — `heuristic` keeps the agenda ordered by
|
|
116
|
+
`g + h`, so only competitive items are expanded.
|
|
117
|
+
4. **Lazy hyperedge generation** — rules are produced by `rules` only when one
|
|
118
|
+
of their premises is finalised, never enumerated up front.
|
|
119
|
+
|
|
120
|
+
## API
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import {
|
|
124
|
+
type DeductionSystem,
|
|
125
|
+
type Derivation,
|
|
126
|
+
lightestDerivation,
|
|
127
|
+
type Rule,
|
|
128
|
+
} from "derive";
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `lightestDerivation<I>(system: DeductionSystem<I>): Derivation<I> | null`
|
|
132
|
+
|
|
133
|
+
Runs the search and returns the root of a lightest derivation tree (`null` if
|
|
134
|
+
the goal cannot be derived). `root.cost` is the total derivation cost; the tree
|
|
135
|
+
is walked through `root.premises`.
|
|
136
|
+
|
|
137
|
+
### `DeductionSystem<I>`
|
|
138
|
+
|
|
139
|
+
The problem you hand the solver. `I` is your item type — anything at all.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
interface DeductionSystem<I> {
|
|
143
|
+
// Canonical key for chart memoization (the item's "boundary signature":
|
|
144
|
+
// everything that can affect how it later combines).
|
|
145
|
+
key(item: I): string;
|
|
146
|
+
|
|
147
|
+
// The atomic items and their base costs — the search's seeds.
|
|
148
|
+
axioms(): Iterable<{ item: I; cost: number }>;
|
|
149
|
+
|
|
150
|
+
// Lazily yield the rules that have `item` among their premises. Called once,
|
|
151
|
+
// when `item` is finalised. `costOf(other)` returns another item's finalised
|
|
152
|
+
// cost (Infinity if not yet known) — use it to drop rules whose other
|
|
153
|
+
// premises are still open or whose conclusion can no longer beat the goal.
|
|
154
|
+
rules(item: I, costOf: (other: I) => number): Iterable<Rule<I>>;
|
|
155
|
+
|
|
156
|
+
// Whether `item` satisfies the goal. The first finalised goal wins.
|
|
157
|
+
isGoal(item: I): boolean;
|
|
158
|
+
|
|
159
|
+
// Optional admissible, consistent lower bound on the cost from `item` to a
|
|
160
|
+
// goal. Omit it (or return 0) for plain Knuth/Dijkstra.
|
|
161
|
+
heuristic?(item: I): number;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `Rule<I>` and `Derivation<I>`
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
interface Rule<I> {
|
|
169
|
+
premises: readonly I[]; // the conjunction (one premise = an OR edge; many = an AND node)
|
|
170
|
+
conclusion: I;
|
|
171
|
+
cost: number; // local cost, non-negative
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface Derivation<I> {
|
|
175
|
+
item: I;
|
|
176
|
+
cost: number; // this item's lightest-derivation cost (its g)
|
|
177
|
+
rule: Rule<I> | null; // the producing rule, or null for an axiom
|
|
178
|
+
premises: Array<Derivation<I>>; // derivations of the rule's premises
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Example: shortest path is a special case
|
|
183
|
+
|
|
184
|
+
A weighted directed graph is a deduction system in which every node is an item
|
|
185
|
+
and every edge is a one-premise rule. The lightest derivation of the target is
|
|
186
|
+
the shortest path — adapted A\*LD collapses to A\*.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { type DeductionSystem, lightestDerivation } from "derive";
|
|
190
|
+
|
|
191
|
+
const edges: Record<string, Array<[string, number]>> = {
|
|
192
|
+
a: [["b", 1], ["c", 4]],
|
|
193
|
+
b: [["c", 1], ["d", 5]],
|
|
194
|
+
c: [["d", 1]],
|
|
195
|
+
d: [],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const shortestPath: DeductionSystem<string> = {
|
|
199
|
+
key: (n) => n,
|
|
200
|
+
*axioms() {
|
|
201
|
+
yield { item: "a", cost: 0 };
|
|
202
|
+
},
|
|
203
|
+
*rules(node) {
|
|
204
|
+
for (const [to, w] of edges[node] ?? []) {
|
|
205
|
+
yield { premises: [node], conclusion: to, cost: w };
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
isGoal: (n) => n === "d",
|
|
209
|
+
heuristic: () => 0, // any admissible lower bound focuses the search
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const best = lightestDerivation(shortestPath);
|
|
213
|
+
// best.cost === 3, and a → b → c → d is recovered from best.premises
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
To compose rather than merely traverse, give a rule **several** premises: the
|
|
217
|
+
search will derive each one and only fire the rule once they are all finalised,
|
|
218
|
+
charging `cost` on top of their combined cost.
|
|
219
|
+
|
|
220
|
+
## Example: a bridge composes two parts into one
|
|
221
|
+
|
|
222
|
+
Two axioms `A` (3) and `B` (4) and a bridge `A ∧ B → AB` (1). The bridge is
|
|
223
|
+
yielded from _either_ premise; the engine waits until both are finalised, then
|
|
224
|
+
charges the join. The only derivation of `AB` costs `3 + 4 + 1 = 8`, and its
|
|
225
|
+
`premises` array holds the derivations of `A` and `B`.
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { type DeductionSystem, lightestDerivation } from "derive";
|
|
229
|
+
|
|
230
|
+
const system: DeductionSystem<string> = {
|
|
231
|
+
key: (s) => s,
|
|
232
|
+
axioms: () => [{ item: "A", cost: 3 }, { item: "B", cost: 4 }],
|
|
233
|
+
isGoal: (s) => s === "AB",
|
|
234
|
+
*rules(item) {
|
|
235
|
+
// Fired from either side; the engine composes once both are known.
|
|
236
|
+
if (item === "A") yield { premises: ["A", "B"], conclusion: "AB", cost: 1 };
|
|
237
|
+
if (item === "B") yield { premises: ["A", "B"], conclusion: "AB", cost: 1 };
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const best = lightestDerivation(system);
|
|
242
|
+
// best.cost === 8, best.premises.length === 2 — it really used the bridge.
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Add a rule `AB ∧ C → ABC` and the bridge chains: the lightest derivation of
|
|
246
|
+
`ABC` composes all three parts, the join cost paid inside the search.
|
|
247
|
+
|
|
248
|
+
## Correctness conditions
|
|
249
|
+
|
|
250
|
+
The engine is correct provided the caller upholds the conditions adapted A\*LD
|
|
251
|
+
requires:
|
|
252
|
+
|
|
253
|
+
- **Non-negative local costs** (more generally, monotone): adding a rule never
|
|
254
|
+
lowers a derivation's cost.
|
|
255
|
+
- **Admissible, consistent heuristic**: `heuristic` never overestimates the cost
|
|
256
|
+
remaining to a goal, and is hyperedge-consistent,
|
|
257
|
+
`h(conclusion) ≤ ruleCost + Σ h(premiseᵢ)`. The default (`0`) is trivially
|
|
258
|
+
consistent and yields plain Knuth/Dijkstra.
|
|
259
|
+
- **Faithful `key`**: two items with the same key must be interchangeable in
|
|
260
|
+
every rule. The key must preserve every part of an item that can affect future
|
|
261
|
+
composition; anything it drops is asserted irrelevant.
|
|
262
|
+
|
|
263
|
+
## Companion utilities
|
|
264
|
+
|
|
265
|
+
Three small, independently useful pieces share the package, all aligned with the
|
|
266
|
+
same "only the work the goal demands" philosophy:
|
|
267
|
+
|
|
268
|
+
- **`Trie<P>`** — a forward prefix matcher used as a _lazy recognition index_.
|
|
269
|
+
`matchesAt(seq, pos)` walks forward from the root and reports every stored
|
|
270
|
+
pattern beginning at `pos` in time proportional to the longest match; a
|
|
271
|
+
position with nothing learned dead-ends at once, and no length bound is
|
|
272
|
+
imposed — the structure of what was inserted is the bound. A cursor API
|
|
273
|
+
(`root`, `step`, `terminal`) supports incremental walks; `scan(seq)` reports
|
|
274
|
+
all matches across all positions.
|
|
275
|
+
|
|
276
|
+
- **`coverSequence<P>(length, candidates)`** — the segmentation primitive: the
|
|
277
|
+
lightest set of non-overlapping spans covering a sequence, computed on the
|
|
278
|
+
engine. It is the principled, corpus-independent replacement for "scan with an
|
|
279
|
+
automaton, then greedily keep the longest non-overlapping matches" — the same
|
|
280
|
+
linear cost, but the _optimal_ cover.
|
|
281
|
+
|
|
282
|
+
- **`MinHeap<T>`** — the allocation-light binary heap the agenda is built on.
|
|
283
|
+
|
|
284
|
+
## Layout
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
derive/
|
|
288
|
+
├── README.md this file
|
|
289
|
+
└── src/
|
|
290
|
+
├── deduction.ts lightestDerivation — the adapted A*LD engine
|
|
291
|
+
├── trie.ts Trie — lazy forward prefix matcher
|
|
292
|
+
├── rewrite.ts coverSequence — optimal sequence cover
|
|
293
|
+
├── priority-queue.ts MinHeap — the agenda's heap
|
|
294
|
+
└── index.ts public surface
|
|
295
|
+
```
|