@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,263 @@
1
+ /**
2
+ * Knuth's lightest-derivation algorithm with an A* outside bound.
3
+ *
4
+ * A *weighted deduction system* (equivalently an implicit AND-OR hypergraph) is
5
+ * a set of items combined by inference rules
6
+ *
7
+ * premise₁ ∧ … ∧ premiseₖ --localCost--> conclusion
8
+ *
9
+ * where a derivation's cost is the sum of the local costs of the rules used.
10
+ * {@link lightestDerivation} finds a minimum-cost derivation of a goal item.
11
+ * The engine is the Dijkstra-like core of Knuth (1977) — an item's cost is
12
+ * final the moment it is popped — extended with an admissible heuristic so that
13
+ * partial derivations which cannot lead cheaply to the goal are never expanded
14
+ * (A* parsing). It is completely generic: it knows nothing of what items are,
15
+ * only how to canonicalise them, enumerate their rules, score them, and test
16
+ * the goal.
17
+ *
18
+ * The four reductions the search relies on:
19
+ * 1. **Canonical chart memoization** — items are keyed by {@link
20
+ * DeductionSystem.key}; equivalent partial derivations collapse to one
21
+ * chart entry, the cheapest.
22
+ * 2. **Backward demand filtering** — {@link DeductionSystem.rules} only emits
23
+ * rules whose conclusion can still reach the goal, so work unrelated to the
24
+ * goal is never generated.
25
+ * 3. **A* lower-bound pruning** — {@link DeductionSystem.heuristic} keeps the
26
+ * agenda ordered by g + h, so only competitive items are expanded.
27
+ * 4. **Lazy hyperedge generation** — rules (including bridges) are produced by
28
+ * `rules` only when one of their premises is finalised, never up front.
29
+ *
30
+ * Correctness conditions (the caller must uphold these):
31
+ * - Local costs are non-negative (more generally, monotone / superior).
32
+ * - The heuristic never overestimates the remaining cost to a goal
33
+ * (admissible) and is hyperedge-consistent:
34
+ * h(conclusion) ≤ ruleCost + Σ h(premiseᵢ)
35
+ * i.e. relaxing a rule cannot decrease f. The default heuristic (0) is
36
+ * trivially consistent and turns the search into plain Knuth/Dijkstra.
37
+ * - {@link DeductionSystem.key} preserves every part of an item that can
38
+ * affect how it later combines (its "boundary signature"); anything the key
39
+ * drops is asserted to be irrelevant to future composition.
40
+ */
41
+
42
+ import { MinHeap } from "./priority-queue.js";
43
+
44
+ /**
45
+ * A weighted deduction rule (a hyperedge): the conjunction of `premises`
46
+ * derives `conclusion` at an additional `cost`.
47
+ */
48
+ export interface Rule<I> {
49
+ premises: readonly I[];
50
+ conclusion: I;
51
+ /** Local (edge) cost added on top of the premises' costs. Non-negative. */
52
+ cost: number;
53
+ /** The combinator this rule's firing uses at its conclusion:
54
+ * • `"min"` (default, omitted) — Knuth/A* proper: the conclusion's cost is
55
+ * the CHEAPEST of any rule that reaches it, every other route discarded.
56
+ * The shortest-path monoid (min, +) that makes the search admissible and
57
+ * output-sensitive.
58
+ * • `"sum"` — evidence pooling: EVERY firing of a sum rule contributes its
59
+ * cost to the SAME conclusion (accumulated in {@link
60
+ * DeductionSystem.pool}), instead of competing to be the one cheapest
61
+ * route. The (+, +) monoid a consensus vote needs — several
62
+ * independent premises corroborating one conclusion — kept deliberately
63
+ * OUT of the min-cost chart: a pooled conclusion is never relaxed into
64
+ * `g`, never enters the agenda, and is never itself a premise — it is a
65
+ * terminal aggregate the caller reads out of `pool` once the search is
66
+ * done. */
67
+ combine?: "min" | "sum";
68
+ }
69
+
70
+ /** One rule's contribution to a pooled (`combine: "sum"`) conclusion — the
71
+ * firing rule and the already-finalised derivations of its premises, so a
72
+ * caller can render each contribution exactly as it would a min-cost step. */
73
+ export interface PooledContribution<I> {
74
+ rule: Rule<I>;
75
+ premises: Array<Derivation<I>>;
76
+ }
77
+
78
+ /** The running aggregate at one pooled conclusion: every sum-mode rule that
79
+ * has fired for it, accumulated. */
80
+ export interface PooledConclusion<I> {
81
+ item: I;
82
+ cost: number;
83
+ contributions: Array<PooledContribution<I>>;
84
+ }
85
+
86
+ /** The problem the solver is given: items, rules, a goal, and a heuristic. */
87
+ export interface DeductionSystem<I> {
88
+ /** Canonical key for chart memoization (the item's boundary signature). */
89
+ key(item: I): string;
90
+
91
+ /** Axioms: the atomic items and their base costs (the search's seeds). */
92
+ axioms(): Iterable<{ item: I; cost: number }>;
93
+
94
+ /**
95
+ * Lazily generate the demanded rules that have `item` among their premises.
96
+ * Called once, when `item` is finalised. `costOf` returns the finalised cost
97
+ * of any item (Infinity if not yet known) — use it for backward-demand /
98
+ * boundary filtering, e.g. drop a rule whose other premises are still open or
99
+ * whose conclusion can no longer beat the best goal.
100
+ */
101
+ rules(item: I, costOf: (other: I) => number): Iterable<Rule<I>>;
102
+
103
+ /** Whether `item` satisfies the goal. The first finalised goal wins. */
104
+ isGoal(item: I): boolean;
105
+
106
+ /** Admissible, consistent lower bound on the cost from `item` to a goal. */
107
+ heuristic?(item: I): number;
108
+
109
+ /** Present only on a system that fires `combine: "sum"` rules — supplied
110
+ * empty, populated in place as the search runs, read back once it returns
111
+ * (typically `null`: a pooling system has no goal to reach, it exhausts its
112
+ * axioms instead — see {@link lightestDerivation}). Absent on every
113
+ * ordinary min-cost system, which is what keeps pooling a zero-cost opt-in:
114
+ * `relax` only takes the pooling branch when a rule declares `combine:
115
+ * "sum"` AND this map is present. */
116
+ pool?: Map<string, PooledConclusion<I>>;
117
+ }
118
+
119
+ /** A node of the reconstructed derivation tree. */
120
+ export interface Derivation<I> {
121
+ /** The derived item. */
122
+ item: I;
123
+ /** This item's minimum derivation cost (its g value). */
124
+ cost: number;
125
+ /** The rule that produced it, or null if it is an axiom. */
126
+ rule: Rule<I> | null;
127
+ /** Derivations of the rule's premises (empty for an axiom). */
128
+ premises: Array<Derivation<I>>;
129
+ }
130
+
131
+ /**
132
+ * Find a lightest derivation of a goal item, or `null` if none exists.
133
+ * `cost` on the returned root is the total derivation cost.
134
+ */
135
+ export function lightestDerivation<I>(
136
+ system: DeductionSystem<I>,
137
+ ): Derivation<I> | null {
138
+ const g = new Map<string, number>(); // best known cost per item
139
+ const proof = new Map<string, Rule<I> | null>(); // producing rule per item
140
+ const items = new Map<string, I>(); // key → the item it stands for
141
+ const hCache = new Map<string, number>();
142
+ const agenda = new MinHeap<{ key: string; g: number }>();
143
+
144
+ const heuristic = system.heuristic;
145
+ const h = (item: I, key: string): number => {
146
+ if (!heuristic) return 0;
147
+ let v = hCache.get(key);
148
+ if (v === undefined) {
149
+ v = heuristic(item);
150
+ hCache.set(key, v);
151
+ }
152
+ return v;
153
+ };
154
+
155
+ const costOf = (item: I): number => g.get(system.key(item)) ?? Infinity;
156
+
157
+ const relax = (item: I, cost: number, rule: Rule<I> | null): void => {
158
+ const key = system.key(item);
159
+ if (rule?.combine === "sum" && system.pool) {
160
+ // Evidence pooling: accumulate this firing rather than compete for the
161
+ // cheapest — see {@link Rule.combine}. The premises are already
162
+ // finalised (the caller only relaxes a rule once every premise's cost
163
+ // is known), so their derivations can be read back immediately.
164
+ const premises = rule.premises.map((p) =>
165
+ reconstruct(p, system, g, proof)
166
+ );
167
+ const prior = system.pool.get(key);
168
+ system.pool.set(key, {
169
+ item,
170
+ cost: (prior?.cost ?? 0) + cost,
171
+ contributions: [...(prior?.contributions ?? []), { rule, premises }],
172
+ });
173
+ return;
174
+ }
175
+ const current = g.get(key);
176
+ if (current === undefined || cost < current) {
177
+ g.set(key, cost);
178
+ proof.set(key, rule);
179
+ items.set(key, item);
180
+ agenda.push(cost + h(item, key), { key, g: cost });
181
+ }
182
+ };
183
+
184
+ for (const { item, cost } of system.axioms()) relax(item, cost, null);
185
+
186
+ while (agenda.size > 0) {
187
+ const { value } = agenda.pop() as { value: { key: string; g: number } };
188
+ const key = value.key;
189
+ // Lazy deletion: an entry is stale if a cheaper derivation has since been
190
+ // recorded for the same item.
191
+ if (value.g !== g.get(key)) continue;
192
+
193
+ const item = items.get(key) as I;
194
+ if (system.isGoal(item)) {
195
+ return reconstruct(item, system, g, proof);
196
+ }
197
+
198
+ for (const rule of system.rules(item, costOf)) {
199
+ let sum = rule.cost;
200
+ let ready = true;
201
+ for (const p of rule.premises) {
202
+ const pc = g.get(system.key(p));
203
+ if (pc === undefined) {
204
+ ready = false;
205
+ break;
206
+ }
207
+ sum += pc;
208
+ }
209
+ if (ready) relax(rule.conclusion, sum, rule);
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+
215
+ function reconstruct<I>(
216
+ item: I,
217
+ system: DeductionSystem<I>,
218
+ g: Map<string, number>,
219
+ proof: Map<string, Rule<I> | null>,
220
+ ): Derivation<I> {
221
+ // Iterative post-order over the derivation hypergraph. In the rewrite
222
+ // search every rule has one premise, so the derivation is a chain whose
223
+ // length equals the number of frontier edges — which, with long inputs,
224
+ // can exceed the call stack. Multi-premise rules (the test-suite bridge
225
+ // case) are handled by the same explicit stack.
226
+ const done = new Map<string, Derivation<I>>();
227
+ const stack: I[] = [item];
228
+
229
+ while (stack.length > 0) {
230
+ const cur = stack[stack.length - 1]; // peek
231
+ const key = system.key(cur);
232
+
233
+ if (done.has(key)) {
234
+ stack.pop();
235
+ continue;
236
+ }
237
+
238
+ const rule = proof.get(key) ?? null;
239
+ const premises = rule?.premises ?? [];
240
+
241
+ // Push any unresolved premises (rightmost first → leftmost resolves first).
242
+ let pending = false;
243
+ for (let i = premises.length - 1; i >= 0; i--) {
244
+ if (!done.has(system.key(premises[i]))) {
245
+ stack.push(premises[i]);
246
+ pending = true;
247
+ }
248
+ }
249
+
250
+ if (!pending) {
251
+ stack.pop(); // this item
252
+ const kids = premises.map((p) => done.get(system.key(p))!);
253
+ done.set(key, {
254
+ item: cur,
255
+ cost: g.get(key) as number,
256
+ rule,
257
+ premises: kids,
258
+ });
259
+ }
260
+ }
261
+
262
+ return done.get(system.key(item))!;
263
+ }
@@ -0,0 +1,24 @@
1
+ // derive — A* lightest-derivation rewrite search.
2
+ //
3
+ // A small, self-contained library for finding minimum-cost derivations in a
4
+ // weighted deduction system (an implicit AND-OR hypergraph), with on-demand
5
+ // (lazy) rule generation and an admissible A* outside bound. It has no
6
+ // dependency on the rest of the codebase and is reusable for any
7
+ // symbolic-rewriting mechanism. See ./../README.md for the design.
8
+
9
+ export { lightestDerivation } from "./deduction.js";
10
+ export type {
11
+ DeductionSystem,
12
+ Derivation,
13
+ PooledConclusion,
14
+ PooledContribution,
15
+ Rule,
16
+ } from "./deduction.js";
17
+
18
+ export { coverSequence } from "./rewrite.js";
19
+ export type { CandidateSpan, Cover } from "./rewrite.js";
20
+
21
+ export { Trie } from "./trie.js";
22
+ export type { Match } from "./trie.js";
23
+
24
+ export { MinHeap } from "./priority-queue.js";
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Binary min-heap keyed by a numeric priority, carrying an arbitrary payload.
3
+ *
4
+ * It is the agenda of the lightest-derivation search, where the priority is the
5
+ * estimate f = g + h. Stale entries are tolerated by the consumer (lazy
6
+ * deletion), so there is no decrease-key: when an item's cost improves it is
7
+ * simply pushed again, and the older, higher-priority copy is recognised as
8
+ * stale and discarded when it surfaces. Parallel arrays (rather than an array
9
+ * of objects) keep the hot path allocation-free.
10
+ */
11
+ export class MinHeap<T> {
12
+ private readonly keys: number[] = [];
13
+ private readonly vals: T[] = [];
14
+
15
+ get size(): number {
16
+ return this.keys.length;
17
+ }
18
+
19
+ push(priority: number, value: T): void {
20
+ const keys = this.keys;
21
+ const vals = this.vals;
22
+ let i = keys.length;
23
+ keys.push(priority);
24
+ vals.push(value);
25
+ while (i > 0) {
26
+ const parent = (i - 1) >> 1;
27
+ if (keys[i] < keys[parent]) {
28
+ const tk = keys[i];
29
+ keys[i] = keys[parent];
30
+ keys[parent] = tk;
31
+ const tv = vals[i];
32
+ vals[i] = vals[parent];
33
+ vals[parent] = tv;
34
+ i = parent;
35
+ } else break;
36
+ }
37
+ }
38
+
39
+ pop(): { priority: number; value: T } | undefined {
40
+ const keys = this.keys;
41
+ const vals = this.vals;
42
+ const n = keys.length;
43
+ if (n === 0) return undefined;
44
+ const top = { priority: keys[0], value: vals[0] };
45
+ const lastKey = keys.pop() as number;
46
+ const lastVal = vals.pop() as T;
47
+ const m = keys.length;
48
+ if (m > 0) {
49
+ keys[0] = lastKey;
50
+ vals[0] = lastVal;
51
+ let i = 0;
52
+ for (;;) {
53
+ const left = 2 * i + 1;
54
+ const right = left + 1;
55
+ let best = i;
56
+ if (left < m && keys[left] < keys[best]) best = left;
57
+ if (right < m && keys[right] < keys[best]) best = right;
58
+ if (best === i) break;
59
+ const tk = keys[i];
60
+ keys[i] = keys[best];
61
+ keys[best] = tk;
62
+ const tv = vals[i];
63
+ vals[i] = vals[best];
64
+ vals[best] = tv;
65
+ i = best;
66
+ }
67
+ }
68
+ return top;
69
+ }
70
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Sequence segmentation on the lightest-derivation engine.
3
+ *
4
+ * The only structure here is the **frontier**: the items are the positions
5
+ * `0…length` of a sequence, and a derivation is a path through them. That is
6
+ * what keeps the search linear in the input rather than quadratic — there is no
7
+ * enumeration of span *pairs* and no chart of O(n²) sub-sequences.
8
+ *
9
+ * Candidate spans are produced **on demand**: when the search finalises a
10
+ * position it asks a {@link Trie} which learned forms begin there. The trie walk
11
+ * stops exactly where the data's patterns stop — a position with nothing learned
12
+ * dead-ends immediately, a position inside a long form walks precisely that far.
13
+ * Nothing is scanned ahead of demand and no length bound is imposed; the
14
+ * structure of what was learned is the bound.
15
+ *
16
+ * {@link coverSequence} is the segmentation primitive: the lightest set of
17
+ * non-overlapping spans covering a sequence. It is the principled,
18
+ * corpus-independent replacement for "scan the whole stream with an automaton,
19
+ * then greedily keep the longest non-overlapping matches" — same linear cost,
20
+ * but the *optimal* cover and only the work the goal demands.
21
+ */
22
+
23
+ import { type DeductionSystem, lightestDerivation } from "./deduction.js";
24
+
25
+ /** A candidate span over a sequence, carrying caller payload. */
26
+ export interface CandidateSpan<P> {
27
+ /** Start offset (inclusive). */
28
+ start: number;
29
+ /** End offset (exclusive); must be > start. */
30
+ end: number;
31
+ /** Relative cost of using this span (default 1). Lower wins ties. */
32
+ weight?: number;
33
+ /** Caller data, returned on the chosen spans. */
34
+ payload: P;
35
+ }
36
+
37
+ export interface Cover<P> {
38
+ /** Chosen non-overlapping spans, left to right. */
39
+ spans: Array<CandidateSpan<P>>;
40
+ /** Symbols covered by the chosen spans. */
41
+ covered: number;
42
+ /** Symbols left uncovered. */
43
+ uncovered: number;
44
+ }
45
+
46
+ /**
47
+ * The lightest non-overlapping cover of `[0, length)` drawn from `candidates`.
48
+ *
49
+ * Primary objective: cover the most symbols (fewest left uncovered). Secondary:
50
+ * least total span weight — with the default unit weight this prefers fewer,
51
+ * longer spans, the optimal analogue of greedy longest-match, but it can return
52
+ * a pair of shorter spans when together they cover more than one long one (which
53
+ * greedy cannot). Modelled as a shortest path over frontier positions: from a
54
+ * position you either leave one symbol uncovered (costly) or take a candidate
55
+ * that starts there (cheap), reaching `length`.
56
+ *
57
+ * Cost: O((length + |candidates|) · log length) — the frontier has `length + 1`
58
+ * items, each finalised once, with one "skip" edge plus the candidates that
59
+ * start there. No quadratic span structure.
60
+ */
61
+ export function coverSequence<P>(
62
+ length: number,
63
+ candidates: ReadonlyArray<CandidateSpan<P>>,
64
+ ): Cover<P> {
65
+ if (length <= 0) return { spans: [], covered: 0, uncovered: 0 };
66
+
67
+ // Candidates indexed by where they start, and by their exact (start,end) edge
68
+ // for reconstruction. Both are O(|candidates|) to build and to read.
69
+ const byStart: Array<Array<CandidateSpan<P>>> = Array.from(
70
+ { length },
71
+ () => [],
72
+ );
73
+ const byEdge = new Map<number, CandidateSpan<P>>();
74
+ let maxWeight = 1;
75
+ for (const c of candidates) {
76
+ if (c.end <= c.start || c.start < 0 || c.end > length) continue;
77
+ const w = c.weight ?? 1;
78
+ if (w > maxWeight) maxWeight = w;
79
+ byStart[c.start].push(c);
80
+ const edge = c.start * (length + 1) + c.end;
81
+ const prev = byEdge.get(edge);
82
+ if (!prev || w < (prev.weight ?? 1)) byEdge.set(edge, c);
83
+ }
84
+
85
+ // One uncovered symbol must outweigh any sum of span weights, so coverage is
86
+ // strictly the primary objective and weight only breaks ties.
87
+ const skipCost = maxWeight * length + 1;
88
+
89
+ const system: DeductionSystem<number> = {
90
+ key: (p) => "" + p,
91
+ axioms: () => [{ item: 0, cost: 0 }],
92
+ isGoal: (p) => p === length,
93
+ // No nonzero admissible bound is available here: a single candidate can
94
+ // cover the whole remainder for unit cost, so any per-symbol estimate would
95
+ // overestimate. With h = 0 this is exact Knuth/Dijkstra over the frontier —
96
+ // optimal, and linear in the positions. (The A* outside bound is for systems
97
+ // whose remaining cost can be genuinely lower-bounded; see the engine.)
98
+ *rules(p) {
99
+ if (p >= length) return;
100
+ yield { premises: [p], conclusion: p + 1, cost: skipCost }; // leave uncovered
101
+ for (const c of byStart[p]) {
102
+ yield { premises: [p], conclusion: c.end, cost: c.weight ?? 1 };
103
+ }
104
+ },
105
+ };
106
+
107
+ const best = lightestDerivation(system);
108
+ if (!best) return { spans: [], covered: 0, uncovered: length };
109
+
110
+ // Walk the frontier chain back to the axiom; an edge p→q is a chosen span iff
111
+ // a candidate spans exactly [p, q) (a covering edge is always cheaper than the
112
+ // skips it replaces, so it appears on the optimal path wherever it is used).
113
+ const spans: Array<CandidateSpan<P>> = [];
114
+ let node: typeof best | undefined = best;
115
+ while (node && node.rule) {
116
+ const to = node.item;
117
+ const from = node.premises[0].item;
118
+ const span = byEdge.get(from * (length + 1) + to);
119
+ if (span) spans.push(span);
120
+ node = node.premises[0];
121
+ }
122
+ spans.reverse();
123
+
124
+ let covered = 0;
125
+ for (const s of spans) covered += s.end - s.start;
126
+ return { spans, covered, uncovered: length - covered };
127
+ }