@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
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
+ ```