@seanhogg/builderforce-memory-engine 2026.9.34 → 2026.10.1

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 (170) hide show
  1. package/dist/evermind/delta_wire.d.ts +10 -1
  2. package/dist/evermind/delta_wire.d.ts.map +1 -1
  3. package/dist/evermind/delta_wire.js +25 -3
  4. package/dist/evermind/delta_wire.js.map +1 -1
  5. package/dist/evermind/index.d.ts +4 -4
  6. package/dist/evermind/index.d.ts.map +1 -1
  7. package/dist/evermind/index.js +2 -2
  8. package/dist/evermind/index.js.map +1 -1
  9. package/dist/evermind/merge.d.ts +25 -0
  10. package/dist/evermind/merge.d.ts.map +1 -1
  11. package/dist/evermind/merge.js +30 -0
  12. package/dist/evermind/merge.js.map +1 -1
  13. package/dist/evermind/package.d.ts +9 -0
  14. package/dist/evermind/package.d.ts.map +1 -1
  15. package/dist/evermind/package.js +20 -0
  16. package/dist/evermind/package.js.map +1 -1
  17. package/dist/index.d.ts +6 -5
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/limbic/index.d.ts +3 -0
  22. package/dist/limbic/index.d.ts.map +1 -1
  23. package/dist/limbic/index.js +3 -0
  24. package/dist/limbic/index.js.map +1 -1
  25. package/dist/limbic/limbic_task.d.ts +22 -0
  26. package/dist/limbic/limbic_task.d.ts.map +1 -0
  27. package/dist/limbic/limbic_task.js +45 -0
  28. package/dist/limbic/limbic_task.js.map +1 -0
  29. package/dist/limbic/limbic_trainer.d.ts +17 -30
  30. package/dist/limbic/limbic_trainer.d.ts.map +1 -1
  31. package/dist/limbic/limbic_trainer.js +38 -144
  32. package/dist/limbic/limbic_trainer.js.map +1 -1
  33. package/dist/ml/classical/ewma.d.ts +32 -0
  34. package/dist/ml/classical/ewma.d.ts.map +1 -0
  35. package/dist/ml/classical/ewma.js +46 -0
  36. package/dist/ml/classical/ewma.js.map +1 -0
  37. package/dist/ml/classical/gbm.d.ts +42 -0
  38. package/dist/ml/classical/gbm.d.ts.map +1 -0
  39. package/dist/ml/classical/gbm.js +116 -0
  40. package/dist/ml/classical/gbm.js.map +1 -0
  41. package/dist/ml/classical/index.d.ts +48 -0
  42. package/dist/ml/classical/index.d.ts.map +1 -0
  43. package/dist/ml/classical/index.js +45 -0
  44. package/dist/ml/classical/index.js.map +1 -0
  45. package/dist/ml/classical/isolation_forest.d.ts +37 -0
  46. package/dist/ml/classical/isolation_forest.d.ts.map +1 -0
  47. package/dist/ml/classical/isolation_forest.js +72 -0
  48. package/dist/ml/classical/isolation_forest.js.map +1 -0
  49. package/dist/ml/classical/kmeans.d.ts +31 -0
  50. package/dist/ml/classical/kmeans.d.ts.map +1 -0
  51. package/dist/ml/classical/kmeans.js +93 -0
  52. package/dist/ml/classical/kmeans.js.map +1 -0
  53. package/dist/ml/classical/knn.d.ts +48 -0
  54. package/dist/ml/classical/knn.d.ts.map +1 -0
  55. package/dist/ml/classical/knn.js +68 -0
  56. package/dist/ml/classical/knn.js.map +1 -0
  57. package/dist/ml/classical/linear.d.ts +29 -0
  58. package/dist/ml/classical/linear.d.ts.map +1 -0
  59. package/dist/ml/classical/linear.js +114 -0
  60. package/dist/ml/classical/linear.js.map +1 -0
  61. package/dist/ml/classical/logistic.d.ts +39 -0
  62. package/dist/ml/classical/logistic.d.ts.map +1 -0
  63. package/dist/ml/classical/logistic.js +103 -0
  64. package/dist/ml/classical/logistic.js.map +1 -0
  65. package/dist/ml/classical/math.d.ts +26 -0
  66. package/dist/ml/classical/math.d.ts.map +1 -0
  67. package/dist/ml/classical/math.js +90 -0
  68. package/dist/ml/classical/math.js.map +1 -0
  69. package/dist/ml/classical/naive_bayes.d.ts +24 -0
  70. package/dist/ml/classical/naive_bayes.d.ts.map +1 -0
  71. package/dist/ml/classical/naive_bayes.js +67 -0
  72. package/dist/ml/classical/naive_bayes.js.map +1 -0
  73. package/dist/ml/classical/robust_z.d.ts +30 -0
  74. package/dist/ml/classical/robust_z.d.ts.map +1 -0
  75. package/dist/ml/classical/robust_z.js +46 -0
  76. package/dist/ml/classical/robust_z.js.map +1 -0
  77. package/dist/ml/dense_head.d.ts +33 -0
  78. package/dist/ml/dense_head.d.ts.map +1 -0
  79. package/dist/ml/dense_head.js +140 -0
  80. package/dist/ml/dense_head.js.map +1 -0
  81. package/dist/ml/features.d.ts +25 -0
  82. package/dist/ml/features.d.ts.map +1 -0
  83. package/dist/ml/features.js +46 -0
  84. package/dist/ml/features.js.map +1 -0
  85. package/dist/ml/federate.d.ts +70 -0
  86. package/dist/ml/federate.d.ts.map +1 -0
  87. package/dist/ml/federate.js +122 -0
  88. package/dist/ml/federate.js.map +1 -0
  89. package/dist/ml/head_trainer.d.ts +76 -0
  90. package/dist/ml/head_trainer.d.ts.map +1 -0
  91. package/dist/ml/head_trainer.js +182 -0
  92. package/dist/ml/head_trainer.js.map +1 -0
  93. package/dist/ml/index.d.ts +23 -0
  94. package/dist/ml/index.d.ts.map +1 -0
  95. package/dist/ml/index.js +11 -0
  96. package/dist/ml/index.js.map +1 -0
  97. package/dist/ml/registry.d.ts +20 -0
  98. package/dist/ml/registry.d.ts.map +1 -0
  99. package/dist/ml/registry.js +43 -0
  100. package/dist/ml/registry.js.map +1 -0
  101. package/dist/ml/run.d.ts +94 -0
  102. package/dist/ml/run.d.ts.map +1 -0
  103. package/dist/ml/run.js +130 -0
  104. package/dist/ml/run.js.map +1 -0
  105. package/dist/ml/tasks/experience.d.ts +90 -0
  106. package/dist/ml/tasks/experience.d.ts.map +1 -0
  107. package/dist/ml/tasks/experience.js +156 -0
  108. package/dist/ml/tasks/experience.js.map +1 -0
  109. package/dist/ml/tasks/index.d.ts +10 -0
  110. package/dist/ml/tasks/index.d.ts.map +1 -0
  111. package/dist/ml/tasks/index.js +28 -0
  112. package/dist/ml/tasks/index.js.map +1 -0
  113. package/dist/ml/tasks/local_success.d.ts +55 -0
  114. package/dist/ml/tasks/local_success.d.ts.map +1 -0
  115. package/dist/ml/tasks/local_success.js +77 -0
  116. package/dist/ml/tasks/local_success.js.map +1 -0
  117. package/dist/ml/tasks/run_anomaly.d.ts +51 -0
  118. package/dist/ml/tasks/run_anomaly.d.ts.map +1 -0
  119. package/dist/ml/tasks/run_anomaly.js +60 -0
  120. package/dist/ml/tasks/run_anomaly.js.map +1 -0
  121. package/dist/ml/train.d.ts +64 -0
  122. package/dist/ml/train.d.ts.map +1 -0
  123. package/dist/ml/train.js +101 -0
  124. package/dist/ml/train.js.map +1 -0
  125. package/dist/ml/types.d.ts +112 -0
  126. package/dist/ml/types.d.ts.map +1 -0
  127. package/dist/ml/types.js +23 -0
  128. package/dist/ml/types.js.map +1 -0
  129. package/dist/moe/moe_model.d.ts.map +1 -1
  130. package/dist/moe/moe_model.js +1 -16
  131. package/dist/moe/moe_model.js.map +1 -1
  132. package/dist/training/cpu_ops.d.ts +2 -0
  133. package/dist/training/cpu_ops.d.ts.map +1 -1
  134. package/dist/training/cpu_ops.js +16 -0
  135. package/dist/training/cpu_ops.js.map +1 -1
  136. package/package.json +1 -1
  137. package/src/evermind/delta_wire.ts +148 -114
  138. package/src/evermind/index.ts +35 -32
  139. package/src/evermind/merge.ts +53 -0
  140. package/src/evermind/package.ts +488 -464
  141. package/src/index.ts +15 -1
  142. package/src/limbic/index.ts +5 -0
  143. package/src/limbic/limbic_task.ts +62 -0
  144. package/src/limbic/limbic_trainer.ts +91 -258
  145. package/src/ml/classical/ewma.ts +68 -0
  146. package/src/ml/classical/gbm.ts +143 -0
  147. package/src/ml/classical/index.ts +76 -0
  148. package/src/ml/classical/isolation_forest.ts +88 -0
  149. package/src/ml/classical/kmeans.ts +102 -0
  150. package/src/ml/classical/knn.ts +98 -0
  151. package/src/ml/classical/linear.ts +119 -0
  152. package/src/ml/classical/logistic.ts +125 -0
  153. package/src/ml/classical/math.ts +93 -0
  154. package/src/ml/classical/naive_bayes.ts +77 -0
  155. package/src/ml/classical/robust_z.ts +65 -0
  156. package/src/ml/dense_head.ts +157 -0
  157. package/src/ml/features.ts +61 -0
  158. package/src/ml/federate.ts +160 -0
  159. package/src/ml/head_trainer.ts +268 -0
  160. package/src/ml/index.ts +57 -0
  161. package/src/ml/registry.ts +44 -0
  162. package/src/ml/run.ts +178 -0
  163. package/src/ml/tasks/experience.ts +214 -0
  164. package/src/ml/tasks/index.ts +30 -0
  165. package/src/ml/tasks/local_success.ts +118 -0
  166. package/src/ml/tasks/run_anomaly.ts +90 -0
  167. package/src/ml/train.ts +136 -0
  168. package/src/ml/types.ts +132 -0
  169. package/src/moe/moe_model.ts +1 -13
  170. package/src/training/cpu_ops.ts +141 -127
@@ -0,0 +1,143 @@
1
+ /**
2
+ * ml/classical/gbm.ts — a small gradient-boosted tree ensemble.
3
+ *
4
+ * Depth-limited regression trees fitted to the loss gradient, one after another,
5
+ * shrunk by a learning rate. Objectives: `binary` (log-loss, Newton leaf values)
6
+ * and `regression` (squared error). Exhaustive split search over midpoints —
7
+ * fine for the catalog's small numeric feature sets, not for wide embeddings.
8
+ */
9
+ import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
10
+ import { featureDim, sigmoid, type Vec } from "./math.js";
11
+
12
+ export type GbmObjective = "binary" | "regression";
13
+
14
+ export interface GbmOptions {
15
+ objective?: GbmObjective;
16
+ trees?: number;
17
+ maxDepth?: number;
18
+ learningRate?: number;
19
+ /** Minimum summed sample weight in a leaf. Default 1. */
20
+ minLeafWeight?: number;
21
+ /** L2 on leaf values (Newton denominator). Default 1. */
22
+ lambda?: number;
23
+ }
24
+
25
+ type RTree = { value: number } | { feature: number; split: number; left: RTree; right: RTree };
26
+
27
+ interface Row {
28
+ x: Vec;
29
+ g: number; // gradient
30
+ h: number; // hessian
31
+ w: number;
32
+ }
33
+
34
+ function leafValue(rows: readonly Row[], lambda: number): number {
35
+ let g = 0;
36
+ let h = 0;
37
+ for (const r of rows) {
38
+ g += r.w * r.g;
39
+ h += r.w * r.h;
40
+ }
41
+ return -g / (h + lambda);
42
+ }
43
+
44
+ /** XGBoost-style structure score G²/(H+λ) of a node. */
45
+ function gain(g: number, h: number, lambda: number): number {
46
+ return (g * g) / (h + lambda);
47
+ }
48
+
49
+ function buildTree(rows: Row[], depth: number, opts: Required<Omit<GbmOptions, "objective" | "trees" | "learningRate">>): RTree {
50
+ if (depth >= opts.maxDepth || rows.length < 2) return { value: leafValue(rows, opts.lambda) };
51
+ let G = 0;
52
+ let H = 0;
53
+ let W = 0;
54
+ for (const r of rows) {
55
+ G += r.w * r.g;
56
+ H += r.w * r.h;
57
+ W += r.w;
58
+ }
59
+ const parent = gain(G, H, opts.lambda);
60
+ let best: { feature: number; split: number; score: number } | null = null;
61
+ const d = rows[0]!.x.length;
62
+ for (let f = 0; f < d; f++) {
63
+ const sorted = [...rows].sort((a, b) => a.x[f]! - b.x[f]!);
64
+ let gl = 0;
65
+ let hl = 0;
66
+ let wl = 0;
67
+ for (let i = 0; i < sorted.length - 1; i++) {
68
+ const r = sorted[i]!;
69
+ gl += r.w * r.g;
70
+ hl += r.w * r.h;
71
+ wl += r.w;
72
+ const a = r.x[f]!;
73
+ const b = sorted[i + 1]!.x[f]!;
74
+ if (a === b) continue;
75
+ if (wl < opts.minLeafWeight || W - wl < opts.minLeafWeight) continue;
76
+ const score = gain(gl, hl, opts.lambda) + gain(G - gl, H - hl, opts.lambda) - parent;
77
+ if (score > 1e-12 && (!best || score > best.score)) best = { feature: f, split: (a + b) / 2, score };
78
+ }
79
+ }
80
+ if (!best) return { value: leafValue(rows, opts.lambda) };
81
+ const left = rows.filter((r) => r.x[best!.feature]! < best!.split);
82
+ const right = rows.filter((r) => r.x[best!.feature]! >= best!.split);
83
+ return { feature: best.feature, split: best.split, left: buildTree(left, depth + 1, opts), right: buildTree(right, depth + 1, opts) };
84
+ }
85
+
86
+ function treeValue(t: RTree, x: Vec): number {
87
+ while (!("value" in t)) t = x[t.feature]! < t.split ? t.left : t.right;
88
+ return t.value;
89
+ }
90
+
91
+ export class GbmModel implements MlPredictor {
92
+ constructor(
93
+ readonly objective: GbmObjective,
94
+ readonly base: number,
95
+ readonly trees: readonly RTree[],
96
+ readonly learningRate: number,
97
+ ) {}
98
+
99
+ /** The raw additive score (log-odds for `binary`). */
100
+ margin(x: Vec): number {
101
+ let s = this.base;
102
+ for (const t of this.trees) s += this.learningRate * treeValue(t, x);
103
+ return s;
104
+ }
105
+
106
+ predict(x: Float32Array): MlPrediction {
107
+ const m = this.margin(x);
108
+ if (this.objective === "regression") return { scores: [m], confidence: 1 };
109
+ const p = sigmoid(m);
110
+ return { scores: [1 - p, p], confidence: Math.max(p, 1 - p) };
111
+ }
112
+ }
113
+
114
+ export function fitGbm(samples: readonly MlSample[], opts: GbmOptions = {}): GbmModel {
115
+ featureDim(samples.map((s) => s.x));
116
+ const objective = opts.objective ?? "binary";
117
+ const lr = opts.learningRate ?? 0.3;
118
+ const treeOpts = { maxDepth: opts.maxDepth ?? 2, minLeafWeight: opts.minLeafWeight ?? 1, lambda: opts.lambda ?? 1 };
119
+
120
+ let wSum = 0;
121
+ let ySum = 0;
122
+ for (const s of samples) {
123
+ wSum += s.weight ?? 1;
124
+ ySum += (s.weight ?? 1) * s.y;
125
+ }
126
+ const yMean = ySum / wSum;
127
+ const base = objective === "binary" ? Math.log(Math.max(yMean, 1e-6) / Math.max(1 - yMean, 1e-6)) : yMean;
128
+ const margins = samples.map(() => base);
129
+ const trees: RTree[] = [];
130
+ for (let t = 0; t < (opts.trees ?? 50); t++) {
131
+ const rows: Row[] = samples.map((s, i) => {
132
+ if (objective === "binary") {
133
+ const p = sigmoid(margins[i]!);
134
+ return { x: s.x, g: p - s.y, h: Math.max(p * (1 - p), 1e-6), w: s.weight ?? 1 };
135
+ }
136
+ return { x: s.x, g: margins[i]! - s.y, h: 1, w: s.weight ?? 1 };
137
+ });
138
+ const tree = buildTree(rows, 0, treeOpts);
139
+ trees.push(tree);
140
+ samples.forEach((s, i) => (margins[i] = margins[i]! + lr * treeValue(tree, s.x)));
141
+ }
142
+ return new GbmModel(objective, base, trees, lr);
143
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ml/classical — pure-TypeScript algorithms, one file each, and the ONE dispatch
3
+ * from a {@link ClassicalAlgorithm} name to its fit.
4
+ */
5
+ import type { ClassicalAlgorithm, MlPredictor, MlSample } from "../types.js";
6
+ import { fitKnn, type KnnOptions } from "./knn.js";
7
+ import { fitNaiveBayes, type NaiveBayesOptions } from "./naive_bayes.js";
8
+ import { fitLogistic, type LogisticOptions } from "./logistic.js";
9
+ import { fitLinear, type LinearOptions } from "./linear.js";
10
+ import { fitKMeans, type KMeansOptions } from "./kmeans.js";
11
+ import { fitRobustZ, type RobustZOptions } from "./robust_z.js";
12
+ import { fitEwma, type EwmaOptions } from "./ewma.js";
13
+ import { fitIsolationForest, type IsolationForestOptions } from "./isolation_forest.js";
14
+ import { fitGbm, type GbmOptions } from "./gbm.js";
15
+
16
+ export { KnnModel, fitKnn } from "./knn.js";
17
+ export type { KnnOptions, KnnMetric, Neighbour } from "./knn.js";
18
+ export { NaiveBayesModel, fitNaiveBayes } from "./naive_bayes.js";
19
+ export type { NaiveBayesOptions } from "./naive_bayes.js";
20
+ export { LogisticModel, fitLogistic } from "./logistic.js";
21
+ export type { LogisticOptions } from "./logistic.js";
22
+ export { LinearModel, fitLinear, solveLinearSystem } from "./linear.js";
23
+ export type { LinearOptions } from "./linear.js";
24
+ export { KMeansModel, fitKMeans } from "./kmeans.js";
25
+ export type { KMeansOptions } from "./kmeans.js";
26
+ export { RobustZModel, fitRobustZ, anomalyConfidence } from "./robust_z.js";
27
+ export type { RobustZOptions } from "./robust_z.js";
28
+ export { EwmaModel, fitEwma } from "./ewma.js";
29
+ export type { EwmaOptions } from "./ewma.js";
30
+ export { IsolationForestModel, fitIsolationForest } from "./isolation_forest.js";
31
+ export type { IsolationForestOptions } from "./isolation_forest.js";
32
+ export { GbmModel, fitGbm } from "./gbm.js";
33
+ export type { GbmOptions, GbmObjective } from "./gbm.js";
34
+ export { cosine, dot } from "./math.js";
35
+
36
+ /** Per-algorithm options, keyed by algorithm name. */
37
+ export interface ClassicalOptions {
38
+ knn?: KnnOptions;
39
+ "naive-bayes"?: NaiveBayesOptions;
40
+ logistic?: LogisticOptions;
41
+ linear?: LinearOptions;
42
+ kmeans?: KMeansOptions;
43
+ "robust-z"?: RobustZOptions;
44
+ ewma?: EwmaOptions;
45
+ "isolation-forest"?: IsolationForestOptions;
46
+ "gbm-small"?: GbmOptions;
47
+ }
48
+
49
+ /** Fit `algorithm` on `samples`. Unsupervised algorithms ignore `y`. */
50
+ export function fitClassical(
51
+ algorithm: ClassicalAlgorithm,
52
+ samples: readonly MlSample[],
53
+ opts: ClassicalOptions = {},
54
+ ): MlPredictor {
55
+ if (samples.length === 0) throw new Error(`fitClassical(${algorithm}): no samples`);
56
+ switch (algorithm) {
57
+ case "knn":
58
+ return fitKnn(samples, opts.knn);
59
+ case "naive-bayes":
60
+ return fitNaiveBayes(samples, opts["naive-bayes"]);
61
+ case "logistic":
62
+ return fitLogistic(samples, opts.logistic);
63
+ case "linear":
64
+ return fitLinear(samples, opts.linear);
65
+ case "kmeans":
66
+ return fitKMeans(samples, opts.kmeans);
67
+ case "robust-z":
68
+ return fitRobustZ(samples, opts["robust-z"]);
69
+ case "ewma":
70
+ return fitEwma(samples, opts.ewma);
71
+ case "isolation-forest":
72
+ return fitIsolationForest(samples, opts["isolation-forest"]);
73
+ case "gbm-small":
74
+ return fitGbm(samples, opts["gbm-small"]);
75
+ }
76
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * ml/classical/isolation_forest.ts — Isolation Forest (Liu, Ting & Zhou 2008).
3
+ *
4
+ * Anomalies are few and different, so random axis-aligned splits isolate them in
5
+ * fewer steps. Score s(x) = 2^(−E[h(x)] / c(ψ)): near 1 = anomalous, ≤ 0.5 =
6
+ * normal. Prediction: `scores[0] = s / threshold` — ≥ 1 is anomalous.
7
+ */
8
+ import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
9
+ import { averagePathLength, featureDim, seededUniform, type Vec } from "./math.js";
10
+ import { anomalyConfidence } from "./robust_z.js";
11
+
12
+ export interface IsolationForestOptions {
13
+ trees?: number;
14
+ /** Subsample size ψ per tree. Default min(256, n). */
15
+ sampleSize?: number;
16
+ /** Raw score at which a point is anomalous. Default 0.6. */
17
+ threshold?: number;
18
+ seed?: number;
19
+ }
20
+
21
+ type ITree = { size: number } | { feature: number; split: number; left: ITree; right: ITree };
22
+
23
+ function buildTree(X: Vec[], depth: number, limit: number, rnd: () => number): ITree {
24
+ if (depth >= limit || X.length <= 1) return { size: X.length };
25
+ const d = X[0]!.length;
26
+ // Only features that still vary can split; none left = an isolated leaf.
27
+ const varying: { f: number; lo: number; hi: number }[] = [];
28
+ for (let f = 0; f < d; f++) {
29
+ let lo = Infinity;
30
+ let hi = -Infinity;
31
+ for (const x of X) {
32
+ lo = Math.min(lo, x[f]!);
33
+ hi = Math.max(hi, x[f]!);
34
+ }
35
+ if (hi > lo) varying.push({ f, lo, hi });
36
+ }
37
+ if (varying.length === 0) return { size: X.length };
38
+ const { f, lo, hi } = varying[Math.floor(rnd() * varying.length)]!;
39
+ const split = lo + rnd() * (hi - lo);
40
+ const left = X.filter((x) => x[f]! < split);
41
+ const right = X.filter((x) => x[f]! >= split);
42
+ return { feature: f, split, left: buildTree(left, depth + 1, limit, rnd), right: buildTree(right, depth + 1, limit, rnd) };
43
+ }
44
+
45
+ function pathLength(tree: ITree, x: Vec, depth = 0): number {
46
+ if ("size" in tree) return depth + averagePathLength(tree.size);
47
+ return pathLength(x[tree.feature]! < tree.split ? tree.left : tree.right, x, depth + 1);
48
+ }
49
+
50
+ export class IsolationForestModel implements MlPredictor {
51
+ constructor(
52
+ readonly trees: readonly ITree[],
53
+ readonly sampleSize: number,
54
+ readonly threshold: number,
55
+ ) {}
56
+
57
+ /** The raw isolation score in (0, 1]. */
58
+ score(x: Vec): number {
59
+ const c = averagePathLength(this.sampleSize);
60
+ if (c === 0) return 0.5;
61
+ const mean = this.trees.reduce((s, t) => s + pathLength(t, x), 0) / this.trees.length;
62
+ return Math.pow(2, -mean / c);
63
+ }
64
+
65
+ predict(x: Float32Array): MlPrediction {
66
+ const s = this.score(x) / this.threshold;
67
+ return { scores: [s], confidence: anomalyConfidence(s) };
68
+ }
69
+ }
70
+
71
+ export function fitIsolationForest(samples: readonly MlSample[], opts: IsolationForestOptions = {}): IsolationForestModel {
72
+ const X = samples.map((s) => s.x);
73
+ featureDim(X);
74
+ const rnd = seededUniform(opts.seed ?? 0x15f0);
75
+ const psi = Math.min(opts.sampleSize ?? 256, X.length);
76
+ const limit = Math.ceil(Math.log2(Math.max(psi, 2)));
77
+ const trees: ITree[] = [];
78
+ for (let t = 0; t < (opts.trees ?? 100); t++) {
79
+ // Sample ψ points without replacement (partial Fisher–Yates over indices).
80
+ const idx = X.map((_, i) => i);
81
+ for (let i = 0; i < psi; i++) {
82
+ const j = i + Math.floor(rnd() * (idx.length - i));
83
+ [idx[i], idx[j]] = [idx[j]!, idx[i]!];
84
+ }
85
+ trees.push(buildTree(idx.slice(0, psi).map((i) => X[i]!), 0, limit, rnd));
86
+ }
87
+ return new IsolationForestModel(trees, psi, opts.threshold ?? 0.6);
88
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * ml/classical/kmeans.ts — k-means clustering (k-means++ seeding, Lloyd iterations).
3
+ *
4
+ * Deterministic for a given seed. A prediction is the nearest centroid; its
5
+ * confidence is how much closer that centroid is than the runner-up.
6
+ */
7
+ import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
8
+ import { featureDim, seededUniform, squaredDistance, type Vec } from "./math.js";
9
+
10
+ export interface KMeansOptions {
11
+ /** Clusters. Default round(√(n/2)), at least 1. */
12
+ k?: number;
13
+ maxIterations?: number;
14
+ seed?: number;
15
+ }
16
+
17
+ export class KMeansModel implements MlPredictor {
18
+ constructor(
19
+ readonly centroids: Float32Array[],
20
+ /** Sum of squared distances of the fitted points to their centroid. */
21
+ readonly inertia: number,
22
+ ) {}
23
+
24
+ /** Nearest centroid index and the two smallest squared distances. */
25
+ assign(x: Vec): { cluster: number; d1: number; d2: number } {
26
+ let cluster = 0;
27
+ let d1 = Infinity;
28
+ let d2 = Infinity;
29
+ this.centroids.forEach((c, i) => {
30
+ const d = squaredDistance(x, c);
31
+ if (d < d1) {
32
+ d2 = d1;
33
+ d1 = d;
34
+ cluster = i;
35
+ } else if (d < d2) d2 = d;
36
+ });
37
+ return { cluster, d1, d2 };
38
+ }
39
+
40
+ predict(x: Float32Array): MlPrediction {
41
+ const { cluster, d1, d2 } = this.assign(x);
42
+ const confidence = Number.isFinite(d2) && d2 > 0 ? 1 - Math.sqrt(d1) / Math.sqrt(d2) : 1;
43
+ return { scores: [cluster], confidence: Math.max(0, Math.min(1, confidence)) };
44
+ }
45
+ }
46
+
47
+ export function fitKMeans(samples: readonly MlSample[], opts: KMeansOptions = {}): KMeansModel {
48
+ const X = samples.map((s) => s.x);
49
+ const d = featureDim(X);
50
+ const k = Math.min(X.length, Math.max(1, opts.k ?? Math.round(Math.sqrt(X.length / 2))));
51
+ const rnd = seededUniform(opts.seed ?? 0x6b6d);
52
+
53
+ // k-means++: first centroid uniformly, the rest ∝ squared distance to the nearest chosen one.
54
+ const centroids: Float32Array[] = [Float32Array.from(X[Math.floor(rnd() * X.length)]!)];
55
+ const nearest = X.map((x) => squaredDistance(x, centroids[0]!));
56
+ while (centroids.length < k) {
57
+ const total = nearest.reduce((a, b) => a + b, 0);
58
+ let pick = 0;
59
+ if (total > 0) {
60
+ let r = rnd() * total;
61
+ for (pick = 0; pick < X.length - 1; pick++) {
62
+ r -= nearest[pick]!;
63
+ if (r <= 0) break;
64
+ }
65
+ } else {
66
+ pick = centroids.length; // every point coincides with a centroid; take the next index
67
+ }
68
+ const c = Float32Array.from(X[pick]!);
69
+ centroids.push(c);
70
+ X.forEach((x, i) => (nearest[i] = Math.min(nearest[i]!, squaredDistance(x, c))));
71
+ }
72
+
73
+ const assign = new Int32Array(X.length).fill(-1);
74
+ const maxIt = opts.maxIterations ?? 100;
75
+ for (let it = 0; it < maxIt; it++) {
76
+ let changed = false;
77
+ const model = new KMeansModel(centroids, 0);
78
+ X.forEach((x, i) => {
79
+ const c = model.assign(x).cluster;
80
+ if (assign[i] !== c) {
81
+ assign[i] = c;
82
+ changed = true;
83
+ }
84
+ });
85
+ if (!changed) break;
86
+ const sums = centroids.map(() => new Float64Array(d));
87
+ const counts = new Array<number>(k).fill(0);
88
+ X.forEach((x, i) => {
89
+ const c = assign[i]!;
90
+ counts[c] = counts[c]! + 1;
91
+ for (let j = 0; j < d; j++) sums[c]![j] = sums[c]![j]! + x[j]!;
92
+ });
93
+ // An emptied cluster keeps its previous centroid.
94
+ centroids.forEach((c, ci) => {
95
+ if (counts[ci]! > 0) for (let j = 0; j < d; j++) c[j] = sums[ci]![j]! / counts[ci]!;
96
+ });
97
+ }
98
+
99
+ let inertia = 0;
100
+ X.forEach((x, i) => (inertia += squaredDistance(x, centroids[assign[i]!]!)));
101
+ return new KMeansModel(centroids, inertia);
102
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * ml/classical/knn.ts — k-nearest-neighbours classification over feature vectors.
3
+ *
4
+ * Votes are similarity-weighted (cosine similarity, or 1/(1+distance) for the
5
+ * euclidean metric), so a close neighbour counts for more than a far one. The
6
+ * same index answers "what is my nearest neighbour?" — the de-duplication query.
7
+ */
8
+ import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
9
+ import { classCount, cosine, featureDim, squaredDistance, topProbability, type Vec } from "./math.js";
10
+
11
+ export type KnnMetric = "cosine" | "euclidean";
12
+
13
+ export interface KnnOptions {
14
+ /** Neighbours that vote. Default 5 (capped at the sample count). */
15
+ k?: number;
16
+ metric?: KnnMetric;
17
+ /** Number of classes (default: inferred from the labels, at least 2). */
18
+ classes?: number;
19
+ }
20
+
21
+ export interface Neighbour {
22
+ index: number;
23
+ /** Higher = closer: cosine similarity, or 1/(1+distance) for euclidean. */
24
+ similarity: number;
25
+ }
26
+
27
+ export class KnnModel implements MlPredictor {
28
+ private readonly _x: Float32Array[] = [];
29
+ private readonly _y: number[] = [];
30
+ private readonly _w: number[] = [];
31
+
32
+ constructor(
33
+ readonly k: number,
34
+ readonly metric: KnnMetric,
35
+ readonly classes: number,
36
+ ) {
37
+ if (!(k >= 1)) throw new Error(`KnnModel: k must be ≥ 1 (got ${k})`);
38
+ }
39
+
40
+ /** How many points the index holds. */
41
+ get size(): number {
42
+ return this._x.length;
43
+ }
44
+
45
+ /** Add one labelled point (online — the index never needs a refit). */
46
+ add(x: Vec, y = 0, weight = 1): void {
47
+ this._x.push(Float32Array.from(x));
48
+ this._y.push(y);
49
+ this._w.push(weight);
50
+ }
51
+
52
+ similarity(a: Vec, b: Vec): number {
53
+ return this.metric === "cosine" ? cosine(a, b) : 1 / (1 + Math.sqrt(squaredDistance(a, b)));
54
+ }
55
+
56
+ /** The `k` closest points, closest first. */
57
+ neighbours(x: Vec, k: number = this.k): Neighbour[] {
58
+ const all = this._x.map((p, index) => ({ index, similarity: this.similarity(x, p) }));
59
+ all.sort((a, b) => b.similarity - a.similarity || a.index - b.index);
60
+ return all.slice(0, Math.min(k, all.length));
61
+ }
62
+
63
+ /** The single closest point, or null when the index is empty. */
64
+ nearest(x: Vec): Neighbour | null {
65
+ return this.neighbours(x, 1)[0] ?? null;
66
+ }
67
+
68
+ /** The label of an indexed point. */
69
+ labelOf(index: number): number {
70
+ return this._y[index]!;
71
+ }
72
+
73
+ /** Class probabilities from similarity-weighted votes (uniform when empty). */
74
+ proba(x: Vec): number[] {
75
+ const votes = new Array<number>(this.classes).fill(0);
76
+ for (const n of this.neighbours(x)) {
77
+ // Shift cosine into (0, 2] so an orthogonal neighbour still casts a small vote.
78
+ const w = (this.metric === "cosine" ? n.similarity + 1 : n.similarity) * this._w[n.index]! + 1e-9;
79
+ votes[this._y[n.index]!]! += w;
80
+ }
81
+ const sum = votes.reduce((a, b) => a + b, 0);
82
+ return sum > 0 ? votes.map((v) => v / sum) : votes.map(() => 1 / this.classes);
83
+ }
84
+
85
+ predict(x: Float32Array): MlPrediction {
86
+ const scores = this.proba(x);
87
+ return { scores, confidence: topProbability(scores) };
88
+ }
89
+ }
90
+
91
+ /** Index `samples` for kNN classification. */
92
+ export function fitKnn(samples: readonly MlSample[], opts: KnnOptions = {}): KnnModel {
93
+ featureDim(samples.map((s) => s.x));
94
+ const classes = opts.classes ?? classCount(samples.map((s) => s.y));
95
+ const model = new KnnModel(Math.min(opts.k ?? 5, samples.length), opts.metric ?? "cosine", classes);
96
+ for (const s of samples) model.add(s.x, s.y, s.weight ?? 1);
97
+ return model;
98
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * ml/classical/linear.ts — ridge linear regression, closed form.
3
+ *
4
+ * Solves (XᵀWX + λI)w = XᵀWy with an unpenalised intercept by Gaussian
5
+ * elimination with partial pivoting. Small dims only (the catalog's numeric
6
+ * tasks), which is exactly where a closed form beats iterating.
7
+ */
8
+ import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
9
+ import { featureDim } from "./math.js";
10
+
11
+ export interface LinearOptions {
12
+ /** L2 penalty on weights (not the intercept). Default 1e-6. */
13
+ l2?: number;
14
+ }
15
+
16
+ export class LinearModel implements MlPredictor {
17
+ constructor(
18
+ readonly weights: Float32Array,
19
+ public intercept: number,
20
+ /** Coefficient of determination on the fitted data, clamped to [0, 1]. */
21
+ readonly r2: number,
22
+ ) {}
23
+
24
+ value(x: ArrayLike<number>): number {
25
+ let s = this.intercept;
26
+ for (let i = 0; i < this.weights.length; i++) s += this.weights[i]! * (x[i] ?? 0);
27
+ return s;
28
+ }
29
+
30
+ predict(x: Float32Array): MlPrediction {
31
+ return { scores: [this.value(x)], confidence: this.r2 };
32
+ }
33
+
34
+ exportParams(): Float32Array {
35
+ const out = new Float32Array(this.weights.length + 1);
36
+ out.set(this.weights, 0);
37
+ out[this.weights.length] = this.intercept;
38
+ return out;
39
+ }
40
+
41
+ importParams(params: ArrayLike<number>): void {
42
+ if (params.length !== this.weights.length + 1) {
43
+ throw new Error(`LinearModel.importParams: expected ${this.weights.length + 1} values, got ${params.length}`);
44
+ }
45
+ for (let i = 0; i < this.weights.length; i++) this.weights[i] = params[i]!;
46
+ this.intercept = params[this.weights.length]!;
47
+ }
48
+ }
49
+
50
+ /** Solve A·x = b in place (A is n×n row-major). Throws when singular. */
51
+ export function solveLinearSystem(A: Float64Array, b: Float64Array, n: number): Float64Array {
52
+ for (let col = 0; col < n; col++) {
53
+ let piv = col;
54
+ for (let r = col + 1; r < n; r++) if (Math.abs(A[r * n + col]!) > Math.abs(A[piv * n + col]!)) piv = r;
55
+ if (Math.abs(A[piv * n + col]!) < 1e-12) throw new Error("solveLinearSystem: singular matrix");
56
+ if (piv !== col) {
57
+ for (let c = 0; c < n; c++) {
58
+ const t = A[col * n + c]!;
59
+ A[col * n + c] = A[piv * n + c]!;
60
+ A[piv * n + c] = t;
61
+ }
62
+ const t = b[col]!;
63
+ b[col] = b[piv]!;
64
+ b[piv] = t;
65
+ }
66
+ for (let r = col + 1; r < n; r++) {
67
+ const f = A[r * n + col]! / A[col * n + col]!;
68
+ if (f === 0) continue;
69
+ for (let c = col; c < n; c++) A[r * n + c] = A[r * n + c]! - f * A[col * n + c]!;
70
+ b[r] = b[r]! - f * b[col]!;
71
+ }
72
+ }
73
+ const x = new Float64Array(n);
74
+ for (let r = n - 1; r >= 0; r--) {
75
+ let s = b[r]!;
76
+ for (let c = r + 1; c < n; c++) s -= A[r * n + c]! * x[c]!;
77
+ x[r] = s / A[r * n + r]!;
78
+ }
79
+ return x;
80
+ }
81
+
82
+ export function fitLinear(samples: readonly MlSample[], opts: LinearOptions = {}): LinearModel {
83
+ const d = featureDim(samples.map((s) => s.x));
84
+ const n = d + 1; // last column = intercept
85
+ const A = new Float64Array(n * n);
86
+ const b = new Float64Array(n);
87
+ const row = new Float64Array(n);
88
+ for (const s of samples) {
89
+ const w = s.weight ?? 1;
90
+ for (let i = 0; i < d; i++) row[i] = s.x[i]!;
91
+ row[d] = 1;
92
+ for (let i = 0; i < n; i++) {
93
+ b[i] = b[i]! + w * row[i]! * s.y;
94
+ for (let j = 0; j < n; j++) A[i * n + j] = A[i * n + j]! + w * row[i]! * row[j]!;
95
+ }
96
+ }
97
+ const l2 = opts.l2 ?? 1e-6;
98
+ for (let i = 0; i < d; i++) A[i * n + i] = A[i * n + i]! + l2;
99
+ const sol = solveLinearSystem(A, b, n);
100
+ const model = new LinearModel(Float32Array.from(sol.subarray(0, d)), sol[d]!, 0);
101
+
102
+ // R² on the fitted data — the confidence the runner reports.
103
+ let wSum = 0;
104
+ let mean = 0;
105
+ for (const s of samples) {
106
+ wSum += s.weight ?? 1;
107
+ mean += (s.weight ?? 1) * s.y;
108
+ }
109
+ mean /= wSum;
110
+ let ssRes = 0;
111
+ let ssTot = 0;
112
+ for (const s of samples) {
113
+ const w = s.weight ?? 1;
114
+ ssRes += w * (s.y - model.value(s.x)) ** 2;
115
+ ssTot += w * (s.y - mean) ** 2;
116
+ }
117
+ const r2 = ssTot > 0 ? Math.min(1, Math.max(0, 1 - ssRes / ssTot)) : ssRes < 1e-12 ? 1 : 0;
118
+ return new LinearModel(model.weights, model.intercept, r2);
119
+ }