@seanhogg/builderforce-memory-engine 2026.9.34 → 2026.10.2

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 (189) hide show
  1. package/dist/evermind/delta_wire.d.ts +26 -2
  2. package/dist/evermind/delta_wire.d.ts.map +1 -1
  3. package/dist/evermind/delta_wire.js +46 -4
  4. package/dist/evermind/delta_wire.js.map +1 -1
  5. package/dist/evermind/index.d.ts +5 -5
  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 +28 -0
  10. package/dist/evermind/merge.d.ts.map +1 -1
  11. package/dist/evermind/merge.js +33 -0
  12. package/dist/evermind/merge.js.map +1 -1
  13. package/dist/evermind/ml_head.d.ts +28 -0
  14. package/dist/evermind/ml_head.d.ts.map +1 -0
  15. package/dist/evermind/ml_head.js +47 -0
  16. package/dist/evermind/ml_head.js.map +1 -0
  17. package/dist/evermind/package.d.ts +9 -0
  18. package/dist/evermind/package.d.ts.map +1 -1
  19. package/dist/evermind/package.js +20 -0
  20. package/dist/evermind/package.js.map +1 -1
  21. package/dist/index.d.ts +6 -5
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +5 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/limbic/index.d.ts +3 -0
  26. package/dist/limbic/index.d.ts.map +1 -1
  27. package/dist/limbic/index.js +3 -0
  28. package/dist/limbic/index.js.map +1 -1
  29. package/dist/limbic/limbic_model.d.ts.map +1 -1
  30. package/dist/limbic/limbic_model.js +1 -3
  31. package/dist/limbic/limbic_model.js.map +1 -1
  32. package/dist/limbic/limbic_task.d.ts +22 -0
  33. package/dist/limbic/limbic_task.d.ts.map +1 -0
  34. package/dist/limbic/limbic_task.js +45 -0
  35. package/dist/limbic/limbic_task.js.map +1 -0
  36. package/dist/limbic/limbic_trainer.d.ts +17 -30
  37. package/dist/limbic/limbic_trainer.d.ts.map +1 -1
  38. package/dist/limbic/limbic_trainer.js +38 -144
  39. package/dist/limbic/limbic_trainer.js.map +1 -1
  40. package/dist/ml/classical/anomaly_composite.d.ts +32 -0
  41. package/dist/ml/classical/anomaly_composite.d.ts.map +1 -0
  42. package/dist/ml/classical/anomaly_composite.js +29 -0
  43. package/dist/ml/classical/anomaly_composite.js.map +1 -0
  44. package/dist/ml/classical/ewma.d.ts +37 -0
  45. package/dist/ml/classical/ewma.d.ts.map +1 -0
  46. package/dist/ml/classical/ewma.js +48 -0
  47. package/dist/ml/classical/ewma.js.map +1 -0
  48. package/dist/ml/classical/gbm.d.ts +42 -0
  49. package/dist/ml/classical/gbm.d.ts.map +1 -0
  50. package/dist/ml/classical/gbm.js +117 -0
  51. package/dist/ml/classical/gbm.js.map +1 -0
  52. package/dist/ml/classical/index.d.ts +52 -0
  53. package/dist/ml/classical/index.d.ts.map +1 -0
  54. package/dist/ml/classical/index.js +49 -0
  55. package/dist/ml/classical/index.js.map +1 -0
  56. package/dist/ml/classical/isolation_forest.d.ts +37 -0
  57. package/dist/ml/classical/isolation_forest.d.ts.map +1 -0
  58. package/dist/ml/classical/isolation_forest.js +72 -0
  59. package/dist/ml/classical/isolation_forest.js.map +1 -0
  60. package/dist/ml/classical/kmeans.d.ts +31 -0
  61. package/dist/ml/classical/kmeans.d.ts.map +1 -0
  62. package/dist/ml/classical/kmeans.js +112 -0
  63. package/dist/ml/classical/kmeans.js.map +1 -0
  64. package/dist/ml/classical/knn.d.ts +51 -0
  65. package/dist/ml/classical/knn.d.ts.map +1 -0
  66. package/dist/ml/classical/knn.js +69 -0
  67. package/dist/ml/classical/knn.js.map +1 -0
  68. package/dist/ml/classical/linear.d.ts +29 -0
  69. package/dist/ml/classical/linear.d.ts.map +1 -0
  70. package/dist/ml/classical/linear.js +114 -0
  71. package/dist/ml/classical/linear.js.map +1 -0
  72. package/dist/ml/classical/logistic.d.ts +31 -0
  73. package/dist/ml/classical/logistic.d.ts.map +1 -0
  74. package/dist/ml/classical/logistic.js +111 -0
  75. package/dist/ml/classical/logistic.js.map +1 -0
  76. package/dist/ml/classical/math.d.ts +26 -0
  77. package/dist/ml/classical/math.d.ts.map +1 -0
  78. package/dist/ml/classical/math.js +90 -0
  79. package/dist/ml/classical/math.js.map +1 -0
  80. package/dist/ml/classical/naive_bayes.d.ts +28 -0
  81. package/dist/ml/classical/naive_bayes.d.ts.map +1 -0
  82. package/dist/ml/classical/naive_bayes.js +72 -0
  83. package/dist/ml/classical/naive_bayes.js.map +1 -0
  84. package/dist/ml/classical/robust_z.d.ts +38 -0
  85. package/dist/ml/classical/robust_z.d.ts.map +1 -0
  86. package/dist/ml/classical/robust_z.js +47 -0
  87. package/dist/ml/classical/robust_z.js.map +1 -0
  88. package/dist/ml/dense_head.d.ts +33 -0
  89. package/dist/ml/dense_head.d.ts.map +1 -0
  90. package/dist/ml/dense_head.js +140 -0
  91. package/dist/ml/dense_head.js.map +1 -0
  92. package/dist/ml/eval_loss.d.ts +15 -0
  93. package/dist/ml/eval_loss.d.ts.map +1 -0
  94. package/dist/ml/eval_loss.js +20 -0
  95. package/dist/ml/eval_loss.js.map +1 -0
  96. package/dist/ml/features.d.ts +25 -0
  97. package/dist/ml/features.d.ts.map +1 -0
  98. package/dist/ml/features.js +46 -0
  99. package/dist/ml/features.js.map +1 -0
  100. package/dist/ml/federate.d.ts +56 -0
  101. package/dist/ml/federate.d.ts.map +1 -0
  102. package/dist/ml/federate.js +99 -0
  103. package/dist/ml/federate.js.map +1 -0
  104. package/dist/ml/head_trainer.d.ts +78 -0
  105. package/dist/ml/head_trainer.d.ts.map +1 -0
  106. package/dist/ml/head_trainer.js +182 -0
  107. package/dist/ml/head_trainer.js.map +1 -0
  108. package/dist/ml/index.d.ts +23 -0
  109. package/dist/ml/index.d.ts.map +1 -0
  110. package/dist/ml/index.js +11 -0
  111. package/dist/ml/index.js.map +1 -0
  112. package/dist/ml/registry.d.ts +20 -0
  113. package/dist/ml/registry.d.ts.map +1 -0
  114. package/dist/ml/registry.js +43 -0
  115. package/dist/ml/registry.js.map +1 -0
  116. package/dist/ml/run.d.ts +103 -0
  117. package/dist/ml/run.d.ts.map +1 -0
  118. package/dist/ml/run.js +169 -0
  119. package/dist/ml/run.js.map +1 -0
  120. package/dist/ml/tasks/experience.d.ts +127 -0
  121. package/dist/ml/tasks/experience.d.ts.map +1 -0
  122. package/dist/ml/tasks/experience.js +213 -0
  123. package/dist/ml/tasks/experience.js.map +1 -0
  124. package/dist/ml/tasks/index.d.ts +10 -0
  125. package/dist/ml/tasks/index.d.ts.map +1 -0
  126. package/dist/ml/tasks/index.js +28 -0
  127. package/dist/ml/tasks/index.js.map +1 -0
  128. package/dist/ml/tasks/local_success.d.ts +55 -0
  129. package/dist/ml/tasks/local_success.d.ts.map +1 -0
  130. package/dist/ml/tasks/local_success.js +77 -0
  131. package/dist/ml/tasks/local_success.js.map +1 -0
  132. package/dist/ml/tasks/run_anomaly.d.ts +52 -0
  133. package/dist/ml/tasks/run_anomaly.d.ts.map +1 -0
  134. package/dist/ml/tasks/run_anomaly.js +50 -0
  135. package/dist/ml/tasks/run_anomaly.js.map +1 -0
  136. package/dist/ml/train.d.ts +59 -0
  137. package/dist/ml/train.d.ts.map +1 -0
  138. package/dist/ml/train.js +84 -0
  139. package/dist/ml/train.js.map +1 -0
  140. package/dist/ml/types.d.ts +115 -0
  141. package/dist/ml/types.d.ts.map +1 -0
  142. package/dist/ml/types.js +24 -0
  143. package/dist/ml/types.js.map +1 -0
  144. package/dist/moe/moe_model.d.ts.map +1 -1
  145. package/dist/moe/moe_model.js +1 -16
  146. package/dist/moe/moe_model.js.map +1 -1
  147. package/dist/training/cpu_ops.d.ts +2 -0
  148. package/dist/training/cpu_ops.d.ts.map +1 -1
  149. package/dist/training/cpu_ops.js +16 -0
  150. package/dist/training/cpu_ops.js.map +1 -1
  151. package/package.json +1 -1
  152. package/src/evermind/delta_wire.ts +63 -4
  153. package/src/evermind/index.ts +7 -3
  154. package/src/evermind/merge.ts +59 -0
  155. package/src/evermind/ml_head.ts +63 -0
  156. package/src/evermind/package.ts +24 -0
  157. package/src/index.ts +17 -1
  158. package/src/limbic/index.ts +5 -0
  159. package/src/limbic/limbic_model.ts +1 -4
  160. package/src/limbic/limbic_task.ts +62 -0
  161. package/src/limbic/limbic_trainer.ts +41 -208
  162. package/src/ml/classical/anomaly_composite.ts +46 -0
  163. package/src/ml/classical/ewma.ts +74 -0
  164. package/src/ml/classical/gbm.ts +144 -0
  165. package/src/ml/classical/index.ts +82 -0
  166. package/src/ml/classical/isolation_forest.ts +88 -0
  167. package/src/ml/classical/kmeans.ts +120 -0
  168. package/src/ml/classical/knn.ts +102 -0
  169. package/src/ml/classical/linear.ts +119 -0
  170. package/src/ml/classical/logistic.ts +125 -0
  171. package/src/ml/classical/math.ts +93 -0
  172. package/src/ml/classical/naive_bayes.ts +84 -0
  173. package/src/ml/classical/robust_z.ts +74 -0
  174. package/src/ml/dense_head.ts +157 -0
  175. package/src/ml/eval_loss.ts +27 -0
  176. package/src/ml/features.ts +61 -0
  177. package/src/ml/federate.ts +130 -0
  178. package/src/ml/head_trainer.ts +247 -0
  179. package/src/ml/index.ts +57 -0
  180. package/src/ml/registry.ts +44 -0
  181. package/src/ml/run.ts +222 -0
  182. package/src/ml/tasks/experience.ts +293 -0
  183. package/src/ml/tasks/index.ts +30 -0
  184. package/src/ml/tasks/local_success.ts +118 -0
  185. package/src/ml/tasks/run_anomaly.ts +84 -0
  186. package/src/ml/train.ts +120 -0
  187. package/src/ml/types.ts +135 -0
  188. package/src/moe/moe_model.ts +1 -13
  189. package/src/training/cpu_ops.ts +14 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * ml/classical/math.ts — the small vector helpers every classical algorithm shares.
3
+ * Pure, allocation-light, deterministic.
4
+ */
5
+ import { SeededRng } from "../../utils/rng.js";
6
+
7
+ /** The engine's ONE sigmoid / softmax (`training/cpu_ops.ts`). */
8
+ export { sigmoid, softmax } from "../../training/cpu_ops.js";
9
+
10
+ export type Vec = ArrayLike<number>;
11
+
12
+ export function dot(a: Vec, b: Vec): number {
13
+ const n = Math.min(a.length, b.length);
14
+ let s = 0;
15
+ for (let i = 0; i < n; i++) s += a[i]! * b[i]!;
16
+ return s;
17
+ }
18
+
19
+ export function norm(a: Vec): number {
20
+ return Math.sqrt(dot(a, a));
21
+ }
22
+
23
+ /** Cosine similarity in [-1, 1]; 0 when either vector is zero. */
24
+ export function cosine(a: Vec, b: Vec): number {
25
+ const na = norm(a);
26
+ const nb = norm(b);
27
+ return na > 0 && nb > 0 ? dot(a, b) / (na * nb) : 0;
28
+ }
29
+
30
+ export function squaredDistance(a: Vec, b: Vec): number {
31
+ const n = Math.min(a.length, b.length);
32
+ let s = 0;
33
+ for (let i = 0; i < n; i++) {
34
+ const d = a[i]! - b[i]!;
35
+ s += d * d;
36
+ }
37
+ return s;
38
+ }
39
+
40
+ /** Median of a numeric list (copy-sorted). NaN for an empty list. */
41
+ export function median(values: readonly number[]): number {
42
+ if (values.length === 0) return Number.NaN;
43
+ const s = [...values].sort((a, b) => a - b);
44
+ const m = s.length >> 1;
45
+ return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
46
+ }
47
+
48
+ /** Index of the largest value (first on ties). */
49
+ export function argmax(values: ArrayLike<number>): number {
50
+ let best = 0;
51
+ for (let i = 1; i < values.length; i++) if (values[i]! > values[best]!) best = i;
52
+ return best;
53
+ }
54
+
55
+ /** Number of distinct classes implied by integer labels 0..k-1. */
56
+ export function classCount(labels: readonly number[]): number {
57
+ let k = 0;
58
+ for (const y of labels) {
59
+ if (!Number.isInteger(y) || y < 0) throw new Error(`class label must be a non-negative integer, got ${y}`);
60
+ k = Math.max(k, y + 1);
61
+ }
62
+ return Math.max(k, 2);
63
+ }
64
+
65
+ /** Feature dimension of a non-empty sample matrix; throws on ragged rows. */
66
+ export function featureDim(X: readonly Vec[]): number {
67
+ if (X.length === 0) throw new Error("no samples");
68
+ const d = X[0]!.length;
69
+ for (const x of X) if (x.length !== d) throw new Error(`ragged features: expected ${d}, got ${x.length}`);
70
+ return d;
71
+ }
72
+
73
+ /** Confidence of a probability vector: its top probability. */
74
+ export function topProbability(p: readonly number[]): number {
75
+ return p.length ? Math.max(...p) : 0;
76
+ }
77
+
78
+ /** A deterministic [0,1) source for algorithms that sample — the engine's ONE seeded RNG. */
79
+ export function seededUniform(seed: number): () => number {
80
+ const rng = new SeededRng(seed);
81
+ return () => rng.next();
82
+ }
83
+
84
+ /**
85
+ * Average path length of an unsuccessful BST search over n points — the
86
+ * isolation-forest normaliser c(n).
87
+ */
88
+ export function averagePathLength(n: number): number {
89
+ if (n <= 1) return 0;
90
+ if (n === 2) return 1;
91
+ const harmonic = Math.log(n - 1) + 0.5772156649;
92
+ return 2 * harmonic - (2 * (n - 1)) / n;
93
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ml/classical/naive_bayes.ts — Gaussian naive Bayes.
3
+ *
4
+ * Per class: a weighted prior, and an independent Gaussian per feature. Variance
5
+ * is smoothed by a fraction of the largest feature variance (as scikit-learn's
6
+ * `var_smoothing`), so a constant feature never divides by zero.
7
+ *
8
+ * A class with no training weight has prior −∞ and probability exactly 0: its
9
+ * Gaussian is a placeholder (mean 0, variance ≈ the smoothing floor), which would
10
+ * otherwise out-score every real class on an input near zero.
11
+ */
12
+ import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
13
+ import { classCount, featureDim, softmax, topProbability } from "./math.js";
14
+
15
+ export interface NaiveBayesOptions {
16
+ classes?: number;
17
+ /** Fraction of the largest feature variance added to every variance. Default 1e-9. */
18
+ varSmoothing?: number;
19
+ }
20
+
21
+ export class NaiveBayesModel implements MlPredictor {
22
+ constructor(
23
+ readonly logPrior: Float64Array,
24
+ readonly mean: Float64Array[],
25
+ readonly variance: Float64Array[],
26
+ ) {}
27
+
28
+ /** Per-class joint log-likelihood log P(c) + Σ log N(x_i | μ, σ²); −∞ for an unseen class. */
29
+ jointLogLikelihood(x: ArrayLike<number>): number[] {
30
+ return this.mean.map((mu, c) => {
31
+ const v = this.variance[c]!;
32
+ let ll = this.logPrior[c]!;
33
+ if (ll === -Infinity) return ll;
34
+ for (let i = 0; i < mu.length; i++) {
35
+ const d = (x[i] ?? 0) - mu[i]!;
36
+ ll += -0.5 * Math.log(2 * Math.PI * v[i]!) - (d * d) / (2 * v[i]!);
37
+ }
38
+ return ll;
39
+ });
40
+ }
41
+
42
+ predict(x: Float32Array): MlPrediction {
43
+ const scores = Array.from(softmax(this.jointLogLikelihood(x)));
44
+ return { scores, confidence: topProbability(scores) };
45
+ }
46
+ }
47
+
48
+ export function fitNaiveBayes(samples: readonly MlSample[], opts: NaiveBayesOptions = {}): NaiveBayesModel {
49
+ const d = featureDim(samples.map((s) => s.x));
50
+ const k = opts.classes ?? classCount(samples.map((s) => s.y));
51
+ const wSum = new Float64Array(k);
52
+ const mean = Array.from({ length: k }, () => new Float64Array(d));
53
+ const variance = Array.from({ length: k }, () => new Float64Array(d));
54
+
55
+ for (const s of samples) {
56
+ const w = s.weight ?? 1;
57
+ wSum[s.y] = wSum[s.y]! + w;
58
+ for (let i = 0; i < d; i++) mean[s.y]![i] = mean[s.y]![i]! + w * s.x[i]!;
59
+ }
60
+ for (let c = 0; c < k; c++) if (wSum[c]! > 0) for (let i = 0; i < d; i++) mean[c]![i] = mean[c]![i]! / wSum[c]!;
61
+ for (const s of samples) {
62
+ const w = s.weight ?? 1;
63
+ for (let i = 0; i < d; i++) {
64
+ const diff = s.x[i]! - mean[s.y]![i]!;
65
+ variance[s.y]![i] = variance[s.y]![i]! + w * diff * diff;
66
+ }
67
+ }
68
+ let maxVar = 0;
69
+ for (let c = 0; c < k; c++) {
70
+ for (let i = 0; i < d; i++) {
71
+ variance[c]![i] = wSum[c]! > 0 ? variance[c]![i]! / wSum[c]! : 0;
72
+ maxVar = Math.max(maxVar, variance[c]![i]!);
73
+ }
74
+ }
75
+ const eps = (opts.varSmoothing ?? 1e-9) * Math.max(maxVar, 1e-12) + 1e-12;
76
+ for (let c = 0; c < k; c++) for (let i = 0; i < d; i++) variance[c]![i] = variance[c]![i]! + eps;
77
+
78
+ const total = wSum.reduce((a, b) => a + b, 0);
79
+ if (!(total > 0)) throw new Error("fitNaiveBayes: the samples carry no weight");
80
+ // An unseen class is masked out (−∞, so softmax gives it exactly 0). At least one
81
+ // class has weight, so softmax always has a finite maximum.
82
+ const logPrior = Float64Array.from(wSum, (w) => (w > 0 ? Math.log(w / total) : -Infinity));
83
+ return new NaiveBayesModel(logPrior, mean, variance);
84
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * ml/classical/robust_z.ts — robust (median/MAD) z-scores for anomaly detection.
3
+ *
4
+ * Per feature: z = |x − median| / (1.4826 · MAD). Median and MAD are not dragged
5
+ * by the very outliers being looked for, unlike mean and standard deviation. When
6
+ * MAD is 0 (more than half the history identical) the mean absolute deviation
7
+ * stands in, then two floors: a fraction of |median| (relative) and `minScale`
8
+ * (absolute, in the feature's own units). Without the absolute one a history that
9
+ * is all zeros — no tool failures, ever — gets a scale of ~1e-9, and the first
10
+ * small non-zero value scores astronomically.
11
+ *
12
+ * Prediction: `scores[0] = max_i z_i / threshold` — ≥ 1 is anomalous.
13
+ */
14
+ import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
15
+ import { featureDim, median } from "./math.js";
16
+
17
+ /** MAD → standard deviation for normally-distributed data. */
18
+ const MAD_TO_SD = 1.4826;
19
+ /** Mean absolute deviation → standard deviation for normally-distributed data. */
20
+ const MEANAD_TO_SD = 1.2533;
21
+
22
+ export interface RobustZOptions {
23
+ /** z at which a feature counts as anomalous. Default 3.5 (Iglewicz & Hoaglin). */
24
+ threshold?: number;
25
+ /** Smallest scale per feature, as a fraction of |median| (plus 1e-9). Default 0.01. */
26
+ minScaleFraction?: number;
27
+ /**
28
+ * Smallest scale per feature in absolute feature units. Default 0 — the algorithm
29
+ * cannot know the units; a task on a fixed scale sets it (`run.anomaly` does).
30
+ */
31
+ minScale?: number;
32
+ }
33
+
34
+ export class RobustZModel implements MlPredictor {
35
+ constructor(
36
+ readonly center: Float64Array,
37
+ readonly scale: Float64Array,
38
+ readonly threshold: number,
39
+ ) {}
40
+
41
+ /** Per-feature robust z-scores (absolute). */
42
+ zScores(x: ArrayLike<number>): number[] {
43
+ return Array.from(this.center, (c, i) => Math.abs((x[i] ?? 0) - c) / this.scale[i]!);
44
+ }
45
+
46
+ predict(x: Float32Array): MlPrediction {
47
+ const score = Math.max(0, ...this.zScores(x)) / this.threshold;
48
+ return { scores: [score], confidence: anomalyConfidence(score) };
49
+ }
50
+ }
51
+
52
+ /** Confidence of an anomaly score normalised so 1 is the threshold: distance from it, capped. */
53
+ export function anomalyConfidence(score: number): number {
54
+ return Math.max(0, Math.min(1, Math.abs(score - 1)));
55
+ }
56
+
57
+ export function fitRobustZ(samples: readonly MlSample[], opts: RobustZOptions = {}): RobustZModel {
58
+ const X = samples.map((s) => s.x);
59
+ const d = featureDim(X);
60
+ const minFrac = opts.minScaleFraction ?? 0.01;
61
+ const minAbs = opts.minScale ?? 0;
62
+ const center = new Float64Array(d);
63
+ const scale = new Float64Array(d);
64
+ for (let i = 0; i < d; i++) {
65
+ const col = X.map((x) => x[i]!);
66
+ const med = median(col);
67
+ const dev = col.map((v) => Math.abs(v - med));
68
+ let s = MAD_TO_SD * median(dev);
69
+ if (!(s > 0)) s = MEANAD_TO_SD * (dev.reduce((a, b) => a + b, 0) / dev.length);
70
+ center[i] = med;
71
+ scale[i] = Math.max(s, minFrac * Math.abs(med) + 1e-9, minAbs);
72
+ }
73
+ return new RobustZModel(center, scale, opts.threshold ?? 3.5);
74
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * ml/dense_head.ts — the catalog's trainable head: linear, or one tanh hidden layer,
3
+ * over a feature vector (usually an experience embedding).
4
+ *
5
+ * It is a {@link TrainableHead} (so the ONE {@link HeadTrainer} fits it, on the
6
+ * GPU when there is one) and an {@link MlPredictor} (so the runner answers with
7
+ * it). Its parameters flatten to one vector — the unit that federates.
8
+ */
9
+ import { gaussianArray, SeededRng } from "../utils/rng.js";
10
+ import { softmax } from "../training/cpu_ops.js";
11
+ import { assignParams, flattenParams, type HeadParam, type TrainableHead } from "./head_trainer.js";
12
+ import type { MlPrediction, MlPredictor, MlSample } from "./types.js";
13
+
14
+ export interface DenseHeadConfig {
15
+ inputDim: number;
16
+ /** Hidden width; 0 = a linear head. */
17
+ hiddenDim: number;
18
+ /** Classes (softmax) or 1 (mse regression). */
19
+ outputDim: number;
20
+ /** `softmax` = classification over `outputDim` classes; `mse` = scalar regression. */
21
+ objective: "softmax" | "mse";
22
+ seed?: number;
23
+ }
24
+
25
+ export class DenseHead implements TrainableHead<MlSample>, MlPredictor {
26
+ readonly config: Required<DenseHeadConfig>;
27
+ private readonly _params: HeadParam[];
28
+ private readonly _grads: HeadParam[];
29
+
30
+ constructor(config: DenseHeadConfig) {
31
+ this.config = { seed: 0x4e4d, ...config };
32
+ const { inputDim, hiddenDim, outputDim, objective } = this.config;
33
+ if (objective === "mse" && outputDim !== 1) throw new Error("DenseHead: mse objective needs outputDim 1");
34
+ if (objective === "softmax" && outputDim < 2) throw new Error("DenseHead: softmax objective needs outputDim ≥ 2");
35
+ const rng = new SeededRng(this.config.seed);
36
+ const next = () => rng.next();
37
+ const inner = hiddenDim > 0 ? hiddenDim : inputDim;
38
+ const shapes: [string, number, number][] = [
39
+ ...(hiddenDim > 0
40
+ ? ([
41
+ ["w1", hiddenDim * inputDim, 1 / Math.sqrt(inputDim)],
42
+ ["b1", hiddenDim, 0],
43
+ ] as [string, number, number][])
44
+ : []),
45
+ ["w2", outputDim * inner, 1 / Math.sqrt(inner)],
46
+ ["b2", outputDim, 0],
47
+ ];
48
+ this._params = shapes.map(([name, n, std]) => ({ name, numel: n, data: std > 0 ? gaussianArray(n, std, next) : new Float32Array(n) }));
49
+ this._grads = shapes.map(([name, n]) => ({ name, numel: n, data: new Float32Array(n) }));
50
+ }
51
+
52
+ private _p(name: string): Float32Array {
53
+ return this._params.find((p) => p.name === name)!.data;
54
+ }
55
+
56
+ private _g(name: string): Float32Array {
57
+ return this._grads.find((p) => p.name === name)!.data;
58
+ }
59
+
60
+ parameters(): HeadParam[] {
61
+ return this._params;
62
+ }
63
+
64
+ gradients(): HeadParam[] {
65
+ return this._grads;
66
+ }
67
+
68
+ zeroGrad(): void {
69
+ for (const g of this._grads) g.data.fill(0);
70
+ }
71
+
72
+ /** Hidden activation (or the input itself for a linear head) and the output logits/value. */
73
+ private _forward(x: ArrayLike<number>): { h: Float32Array; z: Float32Array } {
74
+ const { inputDim, hiddenDim, outputDim } = this.config;
75
+ let h: Float32Array;
76
+ if (hiddenDim > 0) {
77
+ const w1 = this._p("w1");
78
+ const b1 = this._p("b1");
79
+ h = new Float32Array(hiddenDim);
80
+ for (let j = 0; j < hiddenDim; j++) {
81
+ let s = b1[j]!;
82
+ for (let i = 0; i < inputDim; i++) s += w1[j * inputDim + i]! * (x[i] ?? 0);
83
+ h[j] = Math.tanh(s);
84
+ }
85
+ } else {
86
+ h = Float32Array.from({ length: inputDim }, (_, i) => x[i] ?? 0);
87
+ }
88
+ const w2 = this._p("w2");
89
+ const b2 = this._p("b2");
90
+ const z = new Float32Array(outputDim);
91
+ for (let k = 0; k < outputDim; k++) {
92
+ let s = b2[k]!;
93
+ for (let j = 0; j < h.length; j++) s += w2[k * h.length + j]! * h[j]!;
94
+ z[k] = s;
95
+ }
96
+ return { h, z };
97
+ }
98
+
99
+ /** Loss and dLoss/dz of one sample (unweighted). */
100
+ private _lossGrad(z: Float32Array, y: number): { loss: number; dz: Float32Array } {
101
+ if (this.config.objective === "mse") {
102
+ const d = z[0]! - y;
103
+ return { loss: 0.5 * d * d, dz: Float32Array.of(d) };
104
+ }
105
+ const p = softmax(z);
106
+ const dz = Float32Array.from(p, (v, k) => v - (k === y ? 1 : 0));
107
+ return { loss: -Math.log(Math.max(p[y] ?? 0, 1e-12)), dz };
108
+ }
109
+
110
+ loss(s: MlSample): number {
111
+ return (s.weight ?? 1) * this._lossGrad(this._forward(s.x).z, s.y).loss;
112
+ }
113
+
114
+ accumulate(s: MlSample): number {
115
+ const w = s.weight ?? 1;
116
+ const { inputDim, hiddenDim, outputDim } = this.config;
117
+ const { h, z } = this._forward(s.x);
118
+ const { loss, dz } = this._lossGrad(z, s.y);
119
+ const w2 = this._p("w2");
120
+ const gW2 = this._g("w2");
121
+ const gB2 = this._g("b2");
122
+ const dh = new Float32Array(h.length);
123
+ for (let k = 0; k < outputDim; k++) {
124
+ const g = w * dz[k]!;
125
+ gB2[k] = gB2[k]! + g;
126
+ for (let j = 0; j < h.length; j++) {
127
+ gW2[k * h.length + j] = gW2[k * h.length + j]! + g * h[j]!;
128
+ dh[j] = dh[j]! + g * w2[k * h.length + j]!;
129
+ }
130
+ }
131
+ if (hiddenDim > 0) {
132
+ const gW1 = this._g("w1");
133
+ const gB1 = this._g("b1");
134
+ for (let j = 0; j < hiddenDim; j++) {
135
+ const dpre = dh[j]! * (1 - h[j]! * h[j]!);
136
+ gB1[j] = gB1[j]! + dpre;
137
+ for (let i = 0; i < inputDim; i++) gW1[j * inputDim + i] = gW1[j * inputDim + i]! + dpre * (s.x[i] ?? 0);
138
+ }
139
+ }
140
+ return w * loss;
141
+ }
142
+
143
+ predict(x: Float32Array): MlPrediction {
144
+ const { z } = this._forward(x);
145
+ if (this.config.objective === "mse") return { scores: [z[0]!], confidence: 1 };
146
+ const scores = Array.from(softmax(z));
147
+ return { scores, confidence: Math.max(...scores) };
148
+ }
149
+
150
+ exportParams(): Float32Array {
151
+ return flattenParams(this._params);
152
+ }
153
+
154
+ importParams(params: ArrayLike<number>): void {
155
+ assignParams(this._params, params);
156
+ }
157
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ml/eval_loss.ts — the ONE held-out loss for catalog models.
3
+ *
4
+ * What the trainer reports as `holdoutLoss`, what a federated merge is gated on, and
5
+ * what `LogisticModel.loss` returns. A leaf (types only), so a classical algorithm can
6
+ * use it without importing the trainer that imports the algorithms.
7
+ */
8
+ import type { MlPredictor, MlSample, MlTaskKind } from "./types.js";
9
+
10
+ /**
11
+ * Mean loss of `predictor` on `samples` for a task of `kind` — classify: weighted
12
+ * cross-entropy of the true class; regress: weighted ½·squared error. Null for
13
+ * kinds without a supervised loss, or an empty (or zero-weight) set.
14
+ */
15
+ export function mlEvalLoss(kind: MlTaskKind, predictor: MlPredictor, samples: readonly MlSample[]): number | null {
16
+ if (samples.length === 0 || (kind !== "classify" && kind !== "regress")) return null;
17
+ let sum = 0;
18
+ let wSum = 0;
19
+ for (const s of samples) {
20
+ const w = s.weight ?? 1;
21
+ const p = predictor.predict(s.x);
22
+ const loss = kind === "classify" ? -Math.log(Math.max(p.scores[s.y] ?? 0, 1e-12)) : 0.5 * (p.scores[0]! - s.y) ** 2;
23
+ sum += w * loss;
24
+ wSum += w;
25
+ }
26
+ return wSum > 0 ? sum / wSum : null;
27
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ml/features.ts — how an input becomes a feature vector.
3
+ *
4
+ * Text goes through the existing CPU embedder seam ({@link TextEmbedder}, which
5
+ * `EvermindTextEmbedder` implements: the person's own Evermind turns text into
6
+ * an L2-normalised vector with no GPU and no network). Numeric fields are
7
+ * appended as the task supplies them — on a FIXED scale (see {@link logScale}),
8
+ * never one fitted to local data, so a federated weight means the same thing on
9
+ * every machine.
10
+ */
11
+ import type { TextEmbedder } from "../lm/text_embedder.js";
12
+ import type { FeatureSpec } from "./types.js";
13
+
14
+ export type { TextEmbedder } from "../lm/text_embedder.js";
15
+
16
+ /** Feature vector of one input: `[embed(text) …, numeric …]`. */
17
+ export async function extractFeatures<I>(spec: FeatureSpec<I>, input: I, embedder?: TextEmbedder): Promise<Float32Array> {
18
+ const parts: ArrayLike<number>[] = [];
19
+ if (spec.text) {
20
+ if (!embedder) throw new Error("extractFeatures: this task embeds text but no embedder was supplied");
21
+ parts.push(await embedder.embed(spec.text(input)));
22
+ }
23
+ if (spec.numeric) {
24
+ const nums = spec.numeric(input);
25
+ for (const v of nums) if (!Number.isFinite(v)) throw new Error(`extractFeatures: non-finite numeric feature ${v}`);
26
+ parts.push(nums);
27
+ }
28
+ if (parts.length === 0) throw new Error("extractFeatures: the feature spec has neither text nor numeric fields");
29
+ const out = new Float32Array(parts.reduce((n, p) => n + p.length, 0));
30
+ let o = 0;
31
+ for (const p of parts) {
32
+ out.set(p, o);
33
+ o += p.length;
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /** Feature vectors of many inputs, in order. */
39
+ export async function extractFeatureMatrix<I>(
40
+ spec: FeatureSpec<I>,
41
+ inputs: readonly I[],
42
+ embedder?: TextEmbedder,
43
+ ): Promise<Float32Array[]> {
44
+ const out: Float32Array[] = [];
45
+ for (const i of inputs) out.push(await extractFeatures(spec, i, embedder));
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * A count or size on a fixed log scale: log1p(max(v, 0)) / log1p(ref). `ref` is
51
+ * the value that maps to 1 (e.g. 100k tokens), so the scale is the same everywhere.
52
+ */
53
+ export function logScale(v: number, ref: number): number {
54
+ return Math.log1p(Math.max(0, v)) / Math.log1p(Math.max(ref, 1));
55
+ }
56
+
57
+ /** A ratio clamped to [0, max] — fill fractions, success rates. */
58
+ export function clampRatio(numerator: number, denominator: number, max = 4): number {
59
+ if (!(denominator > 0)) return 0;
60
+ return Math.max(0, Math.min(max, numerator / denominator));
61
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * ml/federate.ts — trained ML heads federate like the model's own weights.
3
+ *
4
+ * A head's trained state is ONE flat f32 vector (`MlPredictor.exportParams`),
5
+ * stored as a CRC-trailed checkpoint so it is the same shape the delta pipeline
6
+ * already handles: a learner diffs its trained copy against the version it
7
+ * started from (`diffCheckpoints`), ships it on the existing delta wire with
8
+ * `target: "ml:<task>"`, and the writer folds N of them with the existing FedAvg
9
+ * (`mergeCheckpointDiffsGated` → `mergeCheckpointDiffs`), refusing a merge whose
10
+ * held-out loss regresses. No second wire, no second merge.
11
+ *
12
+ * The head's manifest entry and checkpoint format live with the artifact
13
+ * (`evermind/ml_head.ts`); this module only imports evermind/, never the reverse.
14
+ */
15
+ import {
16
+ buildDeltaLearnPayload,
17
+ decodeDeltaPayload,
18
+ deltaTargetMismatch,
19
+ deltaUnusableReason,
20
+ mlHeadTarget,
21
+ type DeltaLearnPayload,
22
+ } from "../evermind/delta_wire.js";
23
+ import { mergeCheckpointDiffsGated, type GatedMergeResult, type MergeGateOptions } from "../evermind/merge.js";
24
+ import { headCheckpoint, headParams, type EvermindMlHead } from "../evermind/ml_head.js";
25
+ import type { EvermindModelPackage } from "../evermind/package.js";
26
+ import { base64ToBytes, bytesToBase64 } from "../utils/base64.js";
27
+ import { diffCheckpoints } from "../utils/delta.js";
28
+
29
+ export { headCheckpoint, headParams, mlHeadUnusableReason } from "../evermind/ml_head.js";
30
+ export type { EvermindMlHead } from "../evermind/ml_head.js";
31
+
32
+ /** A manifest entry for a head's current parameters. */
33
+ export function mlHeadEntry(
34
+ task: string,
35
+ engine: string,
36
+ version: number,
37
+ params: ArrayLike<number>,
38
+ shape: Record<string, number> = {},
39
+ evalLoss?: number,
40
+ ): EvermindMlHead {
41
+ return {
42
+ task,
43
+ engine,
44
+ version,
45
+ shape,
46
+ params: bytesToBase64(headCheckpoint(params)),
47
+ ...(evalLoss != null && Number.isFinite(evalLoss) ? { evalLoss } : {}),
48
+ };
49
+ }
50
+
51
+ /** The entry's flat parameters. */
52
+ export function mlHeadParams(entry: EvermindMlHead): Float32Array {
53
+ return headParams(base64ToBytes(entry.params));
54
+ }
55
+
56
+ /** The head a package carries for `task`, if any. */
57
+ export function packageMlHead(pkg: EvermindModelPackage, task: string): EvermindMlHead | undefined {
58
+ return pkg.manifest.mlHeads?.find((h) => h.task === task);
59
+ }
60
+
61
+ /** `pkg` with `entry` as its head for `entry.task` (replacing the old one). */
62
+ export function withPackageMlHead(pkg: EvermindModelPackage, entry: EvermindMlHead): EvermindModelPackage {
63
+ return pkg.withMlHeads([...(pkg.manifest.mlHeads ?? []).filter((h) => h.task !== entry.task), entry]);
64
+ }
65
+
66
+ /**
67
+ * The delta-wire payload for a locally trained head: its new parameters diffed
68
+ * against the version it started from. The caller owns the size guard, as for LM deltas.
69
+ */
70
+ export function buildMlHeadDelta(base: EvermindMlHead, trained: ArrayLike<number>, weight?: number, label?: string): DeltaLearnPayload {
71
+ const diff = diffCheckpoints(base64ToBytes(base.params), headCheckpoint(trained));
72
+ return buildDeltaLearnPayload(diff, base.version, weight, label ?? `ml head ${base.task}`, mlHeadTarget(base.task));
73
+ }
74
+
75
+ export interface MlHeadMergeOutcome {
76
+ /** The head after the round: version + 1 when accepted, the base unchanged when refused. */
77
+ head: EvermindMlHead;
78
+ merge: GatedMergeResult;
79
+ /** Contributions left out of the batch, by index, with why. */
80
+ rejected: { index: number; reason: string }[];
81
+ }
82
+
83
+ /**
84
+ * One federated round for a head: validate each contribution ON ITS OWN (wrong
85
+ * target, stale base or a malformed diff costs only itself), FedAvg the rest, and
86
+ * keep the result only if `score` (held-out loss of a parameter vector, lower is
87
+ * better) does not regress.
88
+ *
89
+ * Fails CLOSED: `requireEval` defaults to true here, so a round `score` cannot judge
90
+ * (null — e.g. `mlEvalLoss` for an anomaly/cluster head or an empty holdout — or a
91
+ * non-finite loss) is refused. A caller that federates an unscorable head on purpose
92
+ * passes `{ requireEval: false }` explicitly.
93
+ */
94
+ export function mergeMlHeadDeltas(
95
+ base: EvermindMlHead,
96
+ payloads: readonly DeltaLearnPayload[],
97
+ score: (params: Float32Array) => number | null,
98
+ opts: MergeGateOptions = {},
99
+ ): MlHeadMergeOutcome {
100
+ const baseCp = base64ToBytes(base.params);
101
+ const target = mlHeadTarget(base.task);
102
+ const diffs: ArrayBuffer[] = [];
103
+ const weights: number[] = [];
104
+ const rejected: { index: number; reason: string }[] = [];
105
+ payloads.forEach((p, index) => {
106
+ const wrongTarget = deltaTargetMismatch(p.target, target);
107
+ if (wrongTarget) return void rejected.push({ index, reason: wrongTarget });
108
+ if (p.baseVersion !== base.version) return void rejected.push({ index, reason: `stale base version ${p.baseVersion} (head is ${base.version})` });
109
+ let diff: ArrayBuffer;
110
+ try {
111
+ diff = decodeDeltaPayload(p);
112
+ } catch (error) {
113
+ return void rejected.push({ index, reason: `diff is not base64: ${error instanceof Error ? error.message : String(error)}` });
114
+ }
115
+ const why = deltaUnusableReason(diff, baseCp);
116
+ if (why) return void rejected.push({ index, reason: why });
117
+ diffs.push(diff);
118
+ weights.push(p.weight ?? 1);
119
+ });
120
+
121
+ const merge = mergeCheckpointDiffsGated(baseCp, diffs, weights, (cp) => score(headParams(cp)), { ...opts, requireEval: opts.requireEval ?? true });
122
+ if (!merge.accepted) return { head: base, merge, rejected };
123
+ const head: EvermindMlHead = {
124
+ ...base,
125
+ version: base.version + 1,
126
+ params: bytesToBase64(merge.checkpoint),
127
+ ...(merge.newLoss != null ? { evalLoss: merge.newLoss } : {}),
128
+ };
129
+ return { head, merge, rejected };
130
+ }