@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
@@ -1,114 +1,148 @@
1
- /**
2
- * evermind/delta_wire.ts — the wire contract of a pre-diffed weight-delta contribution.
3
- *
4
- * A learner with CPU to spare adapts a private copy of a model version, diffs it
5
- * against that version (`diffCheckpoints`) and ships only the sparse diff; the single
6
- * writer then merges it (`mergeCheckpointDiffs`) instead of running the fit.
7
- * Everything the PRODUCER and the WRITER must agree on lives here, once:
8
- * - the payload shape (`diff` base64 + integer `baseVersion`, optional weight/label);
9
- * - the size cap, so a producer can fall back before it is refused;
10
- * - the structural check a delta must pass before it may join a merge batch.
11
- *
12
- * That last one is load-bearing: `mergeCheckpointDiffs` throws on a malformed diff,
13
- * and one throw drops the whole batch. A single bad push must cost only itself.
14
- */
15
- import { deserializeRowDelta } from "../utils/delta.js";
16
- import { verifyCrcTrailer } from "../utils/crc32.js";
17
- import { base64ToBytes, bytesToBase64 } from "../utils/base64.js";
18
-
19
- /** Max accepted base64 delta, in characters (~8 MiB). */
20
- export const MAX_DELTA_B64_CHARS = 8 * 1024 * 1024;
21
-
22
- /** Chars of provenance label kept — a diff carries no text, so the label is its only description. */
23
- export const DELTA_LABEL_MAX_CHARS = 800;
24
-
25
- /** Standard base64 alphabet with optional padding. */
26
- const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
27
-
28
- /** A validated delta contribution — the exact body a producer sends. */
29
- export interface DeltaLearnPayload {
30
- /** base64 serialized RowDelta (`diffCheckpoints(base, adapted)`). */
31
- diff: string;
32
- /** The model version the delta was diffed against. */
33
- baseVersion: number;
34
- /** FedAvg sample weight (> 0). Absent means the writer's default (1). */
35
- weight?: number;
36
- /** Provenance for the merged row (e.g. the run's task). */
37
- label?: string;
38
- }
39
-
40
- /** Why a body was refused: `invalid` (malformed) or `too-large` (over {@link MAX_DELTA_B64_CHARS}). */
41
- export type DeltaParseResult =
42
- | { ok: true; payload: DeltaLearnPayload }
43
- | { ok: false; reason: "invalid" | "too-large"; error: string };
44
-
45
- /**
46
- * Parse + validate a delta body. Pure — a front door calls it to refuse a bad body
47
- * early, and the single writer calls it again because it trusts no caller.
48
- */
49
- export function parseDeltaLearnPayload(raw: unknown): DeltaParseResult {
50
- const body = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
51
- const diff = typeof body.diff === "string" ? body.diff : "";
52
- const baseVersion = typeof body.baseVersion === "number" ? body.baseVersion : Number.NaN;
53
- if (!diff || !Number.isInteger(baseVersion)) {
54
- return {
55
- ok: false,
56
- reason: "invalid",
57
- error: "diff (base64) and baseVersion (integer head version the delta was taken against) are required",
58
- };
59
- }
60
- if (diff.length > MAX_DELTA_B64_CHARS) {
61
- return { ok: false, reason: "too-large", error: `delta too large (max ${MAX_DELTA_B64_CHARS} base64 characters)` };
62
- }
63
- if (diff.length % 4 !== 0 || !BASE64_RE.test(diff)) {
64
- return { ok: false, reason: "invalid", error: "diff must be standard base64" };
65
- }
66
- const weight = typeof body.weight === "number" && Number.isFinite(body.weight) && body.weight > 0 ? body.weight : undefined;
67
- const label = typeof body.label === "string" && body.label.trim() ? body.label.trim().slice(0, DELTA_LABEL_MAX_CHARS) : undefined;
68
- return {
69
- ok: true,
70
- payload: { diff, baseVersion, ...(weight !== undefined ? { weight } : {}), ...(label ? { label } : {}) },
71
- };
72
- }
73
-
74
- /**
75
- * Build the payload for one computed diff. The caller owns the size guard (compare
76
- * `payload.diff.length` with {@link MAX_DELTA_B64_CHARS}) so it can choose a fallback.
77
- */
78
- export function buildDeltaLearnPayload(diff: ArrayBuffer, baseVersion: number, weight?: number, label?: string): DeltaLearnPayload {
79
- const trimmed = (label ?? "").trim();
80
- return {
81
- diff: bytesToBase64(diff),
82
- baseVersion,
83
- ...(typeof weight === "number" && Number.isFinite(weight) && weight > 0 ? { weight } : {}),
84
- ...(trimmed ? { label: trimmed.slice(0, DELTA_LABEL_MAX_CHARS) } : {}),
85
- };
86
- }
87
-
88
- /** Decode a payload's base64 `diff` to the serialized RowDelta bytes. Throws on malformed base64. */
89
- export function decodeDeltaPayload(payload: Pick<DeltaLearnPayload, "diff">): ArrayBuffer {
90
- return base64ToBytes(payload.diff);
91
- }
92
-
93
- /**
94
- * Structural check of ONE delta against the base it claims to diff: it must
95
- * deserialize, be element-granular, index only elements the base has, and carry
96
- * finite values. Returns why it is unusable, or null when it can join a merge.
97
- */
98
- export function deltaUnusableReason(delta: ArrayBuffer, baseCheckpoint: ArrayBuffer): string | null {
99
- try {
100
- const rd = deserializeRowDelta(delta);
101
- if (rd.rowSize !== 1) return `expected an element-granular delta (rowSize 1), got ${rd.rowSize}`;
102
- const elements = verifyCrcTrailer(baseCheckpoint).body.byteLength / 4;
103
- for (let i = 0; i < rd.rows.length; i++) {
104
- const idx = rd.rows[i]!;
105
- if (!Number.isInteger(idx) || idx < 0 || idx >= elements) {
106
- return `row index ${idx} is outside the base checkpoint (${elements} elements)`;
107
- }
108
- if (!Number.isFinite(rd.data[i]!)) return `non-finite value at row ${idx}`;
109
- }
110
- return null;
111
- } catch (error) {
112
- return `not a serialized RowDelta: ${error instanceof Error ? error.message : String(error)}`;
113
- }
114
- }
1
+ /**
2
+ * evermind/delta_wire.ts — the wire contract of a pre-diffed weight-delta contribution.
3
+ *
4
+ * A learner with CPU to spare adapts a private copy of a model version, diffs it
5
+ * against that version (`diffCheckpoints`) and ships only the sparse diff; the single
6
+ * writer then merges it (`mergeCheckpointDiffs`) instead of running the fit.
7
+ * Everything the PRODUCER and the WRITER must agree on lives here, once:
8
+ * - the payload shape (`diff` base64 + integer `baseVersion`, optional weight/label,
9
+ * optional `target` naming WHICH checkpoint the diff is against — the model's own
10
+ * weights, or one trained ML head carried in the manifest's `mlHeads`);
11
+ * - the size cap, so a producer can fall back before it is refused;
12
+ * - the structural check a delta must pass before it may join a merge batch.
13
+ *
14
+ * That last one is load-bearing: `mergeCheckpointDiffs` throws on a malformed diff,
15
+ * and one throw drops the whole batch. A single bad push must cost only itself.
16
+ */
17
+ import { deserializeRowDelta } from "../utils/delta.js";
18
+ import { verifyCrcTrailer } from "../utils/crc32.js";
19
+ import { base64ToBytes, bytesToBase64 } from "../utils/base64.js";
20
+ import { ML_TASK_ID_RE } from "../ml/types.js";
21
+
22
+ /** Max accepted base64 delta, in characters (~8 MiB). */
23
+ export const MAX_DELTA_B64_CHARS = 8 * 1024 * 1024;
24
+
25
+ /** Chars of provenance label kept — a diff carries no text, so the label is its only description. */
26
+ export const DELTA_LABEL_MAX_CHARS = 800;
27
+
28
+ /** A delta's target: absent = the model's own checkpoint; `ml:<task id>` = that trained ML head. */
29
+ export const DELTA_TARGET_RE = new RegExp(`^ml:${ML_TASK_ID_RE.source.slice(1)}`);
30
+
31
+ /** The delta target of the trained head for catalog task `taskId`. */
32
+ export function mlHeadTarget(taskId: string): string {
33
+ return `ml:${taskId}`;
34
+ }
35
+
36
+ /** Standard base64 alphabet with optional padding. */
37
+ const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
38
+
39
+ /** A validated delta contribution — the exact body a producer sends. */
40
+ export interface DeltaLearnPayload {
41
+ /** base64 serialized RowDelta (`diffCheckpoints(base, adapted)`). */
42
+ diff: string;
43
+ /** The model version the delta was diffed against. */
44
+ baseVersion: number;
45
+ /** FedAvg sample weight (> 0). Absent means the writer's default (1). */
46
+ weight?: number;
47
+ /** Provenance for the merged row (e.g. the run's task). */
48
+ label?: string;
49
+ /**
50
+ * Which checkpoint the diff is against. Absent = the model's own weights (every
51
+ * delta written before heads federated); `ml:<task id>` = that trained ML head.
52
+ */
53
+ target?: string;
54
+ }
55
+
56
+ /** Why a body was refused: `invalid` (malformed) or `too-large` (over {@link MAX_DELTA_B64_CHARS}). */
57
+ export type DeltaParseResult =
58
+ | { ok: true; payload: DeltaLearnPayload }
59
+ | { ok: false; reason: "invalid" | "too-large"; error: string };
60
+
61
+ /**
62
+ * Parse + validate a delta body. Pure — a front door calls it to refuse a bad body
63
+ * early, and the single writer calls it again because it trusts no caller.
64
+ */
65
+ export function parseDeltaLearnPayload(raw: unknown): DeltaParseResult {
66
+ const body = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
67
+ const diff = typeof body.diff === "string" ? body.diff : "";
68
+ const baseVersion = typeof body.baseVersion === "number" ? body.baseVersion : Number.NaN;
69
+ if (!diff || !Number.isInteger(baseVersion)) {
70
+ return {
71
+ ok: false,
72
+ reason: "invalid",
73
+ error: "diff (base64) and baseVersion (integer head version the delta was taken against) are required",
74
+ };
75
+ }
76
+ if (diff.length > MAX_DELTA_B64_CHARS) {
77
+ return { ok: false, reason: "too-large", error: `delta too large (max ${MAX_DELTA_B64_CHARS} base64 characters)` };
78
+ }
79
+ if (diff.length % 4 !== 0 || !BASE64_RE.test(diff)) {
80
+ return { ok: false, reason: "invalid", error: "diff must be standard base64" };
81
+ }
82
+ const weight = typeof body.weight === "number" && Number.isFinite(body.weight) && body.weight > 0 ? body.weight : undefined;
83
+ const label = typeof body.label === "string" && body.label.trim() ? body.label.trim().slice(0, DELTA_LABEL_MAX_CHARS) : undefined;
84
+ if (body.target !== undefined && (typeof body.target !== "string" || !DELTA_TARGET_RE.test(body.target))) {
85
+ return { ok: false, reason: "invalid", error: "target, when present, must be ml:<task id>" };
86
+ }
87
+ const target = body.target as string | undefined;
88
+ return {
89
+ ok: true,
90
+ payload: {
91
+ diff,
92
+ baseVersion,
93
+ ...(weight !== undefined ? { weight } : {}),
94
+ ...(label ? { label } : {}),
95
+ ...(target ? { target } : {}),
96
+ },
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Build the payload for one computed diff. The caller owns the size guard (compare
102
+ * `payload.diff.length` with {@link MAX_DELTA_B64_CHARS}) so it can choose a fallback.
103
+ */
104
+ export function buildDeltaLearnPayload(
105
+ diff: ArrayBuffer,
106
+ baseVersion: number,
107
+ weight?: number,
108
+ label?: string,
109
+ target?: string,
110
+ ): DeltaLearnPayload {
111
+ const trimmed = (label ?? "").trim();
112
+ if (target !== undefined && !DELTA_TARGET_RE.test(target)) throw new Error(`buildDeltaLearnPayload: bad target '${target}'`);
113
+ return {
114
+ diff: bytesToBase64(diff),
115
+ baseVersion,
116
+ ...(typeof weight === "number" && Number.isFinite(weight) && weight > 0 ? { weight } : {}),
117
+ ...(trimmed ? { label: trimmed.slice(0, DELTA_LABEL_MAX_CHARS) } : {}),
118
+ ...(target ? { target } : {}),
119
+ };
120
+ }
121
+
122
+ /** Decode a payload's base64 `diff` to the serialized RowDelta bytes. Throws on malformed base64. */
123
+ export function decodeDeltaPayload(payload: Pick<DeltaLearnPayload, "diff">): ArrayBuffer {
124
+ return base64ToBytes(payload.diff);
125
+ }
126
+
127
+ /**
128
+ * Structural check of ONE delta against the base it claims to diff: it must
129
+ * deserialize, be element-granular, index only elements the base has, and carry
130
+ * finite values. Returns why it is unusable, or null when it can join a merge.
131
+ */
132
+ export function deltaUnusableReason(delta: ArrayBuffer, baseCheckpoint: ArrayBuffer): string | null {
133
+ try {
134
+ const rd = deserializeRowDelta(delta);
135
+ if (rd.rowSize !== 1) return `expected an element-granular delta (rowSize 1), got ${rd.rowSize}`;
136
+ const elements = verifyCrcTrailer(baseCheckpoint).body.byteLength / 4;
137
+ for (let i = 0; i < rd.rows.length; i++) {
138
+ const idx = rd.rows[i]!;
139
+ if (!Number.isInteger(idx) || idx < 0 || idx >= elements) {
140
+ return `row index ${idx} is outside the base checkpoint (${elements} elements)`;
141
+ }
142
+ if (!Number.isFinite(rd.data[i]!)) return `non-finite value at row ${idx}`;
143
+ }
144
+ return null;
145
+ } catch (error) {
146
+ return `not a serialized RowDelta: ${error instanceof Error ? error.message : String(error)}`;
147
+ }
148
+ }
@@ -1,32 +1,35 @@
1
- /**
2
- * Evermind — the engine half of the Evermind module: the portable `.evermind`
3
- * artifact plus the learning pipeline every learner shares (adapt, diff wire
4
- * contract, FedAvg merge, pre/post-merge eval).
5
- */
6
- export { EvermindModelPackage, PACKAGE_HEADER_BYTES } from "./package.js";
7
- export type {
8
- EvermindModelManifest,
9
- EvermindModelCard,
10
- EvermindModelType,
11
- EvermindModality,
12
- PackageMeta,
13
- ValidationResult,
14
- } from "./package.js";
15
-
16
- export { adaptAndDiff, adaptPackage, EVERMIND_ADAPT_MAX_CHARS } from "./adapt.js";
17
- export type { AdaptResult, AdaptedPackage, AdaptOptions, AdaptTokenizer } from "./adapt.js";
18
-
19
- export {
20
- parseDeltaLearnPayload,
21
- buildDeltaLearnPayload,
22
- decodeDeltaPayload,
23
- deltaUnusableReason,
24
- MAX_DELTA_B64_CHARS,
25
- } from "./delta_wire.js";
26
- export type { DeltaLearnPayload, DeltaParseResult } from "./delta_wire.js";
27
-
28
- export { mergeCheckpointDiffs } from "./merge.js";
29
- export type { MergeResult } from "./merge.js";
30
-
31
- export { meanEvalLoss } from "./eval.js";
32
- export type { EvalExample, EvalTokenizer } from "./eval.js";
1
+ /**
2
+ * Evermind — the engine half of the Evermind module: the portable `.evermind`
3
+ * artifact plus the learning pipeline every learner shares (adapt, diff wire
4
+ * contract, FedAvg merge, pre/post-merge eval).
5
+ */
6
+ export { EvermindModelPackage, PACKAGE_HEADER_BYTES } from "./package.js";
7
+ export type {
8
+ EvermindModelManifest,
9
+ EvermindModelCard,
10
+ EvermindModelType,
11
+ EvermindModality,
12
+ PackageMeta,
13
+ ValidationResult,
14
+ EvermindMlHead,
15
+ } from "./package.js";
16
+
17
+ export { adaptAndDiff, adaptPackage, EVERMIND_ADAPT_MAX_CHARS } from "./adapt.js";
18
+ export type { AdaptResult, AdaptedPackage, AdaptOptions, AdaptTokenizer } from "./adapt.js";
19
+
20
+ export {
21
+ parseDeltaLearnPayload,
22
+ buildDeltaLearnPayload,
23
+ decodeDeltaPayload,
24
+ deltaUnusableReason,
25
+ MAX_DELTA_B64_CHARS,
26
+ DELTA_TARGET_RE,
27
+ mlHeadTarget,
28
+ } from "./delta_wire.js";
29
+ export type { DeltaLearnPayload, DeltaParseResult } from "./delta_wire.js";
30
+
31
+ export { mergeCheckpointDiffs, mergeCheckpointDiffsGated } from "./merge.js";
32
+ export type { MergeResult, GatedMergeResult, MergeGateOptions } from "./merge.js";
33
+
34
+ export { meanEvalLoss } from "./eval.js";
35
+ export type { EvalExample, EvalTokenizer } from "./eval.js";
@@ -104,3 +104,56 @@ export function mergeCheckpointDiffs(
104
104
  const checkpoint = applyCheckpointDiff(baseCheckpoint, serializeRowDelta(merged));
105
105
  return { checkpoint, mergedRows: rows.length, contributors: parsed.length, deltaNorm: Math.sqrt(sumSq) };
106
106
  }
107
+
108
+ /** A merge that passed (or failed) its held-out regression gate. */
109
+ export interface GatedMergeResult extends MergeResult {
110
+ /** True when the merged checkpoint was kept. When false, `checkpoint` is the BASE. */
111
+ accepted: boolean;
112
+ /** Held-out loss of the base / the merged candidate (null when unscorable). */
113
+ baseLoss: number | null;
114
+ newLoss: number | null;
115
+ /** Why the merge was refused. */
116
+ refusedReason?: "nothing-merged" | "regressed" | "unscored";
117
+ }
118
+
119
+ export interface MergeGateOptions {
120
+ /** Loss increase still accepted (absolute). Default 0 — any regression refuses. */
121
+ tolerance?: number;
122
+ /** Refuse when either side cannot be scored. Default false (an unscored merge is kept). */
123
+ requireEval?: boolean;
124
+ }
125
+
126
+ /**
127
+ * {@link mergeCheckpointDiffs}, then the pre/post held-out check that LM merges
128
+ * record (`meanEvalLoss` of the previous vs the merged version) — enforced: a merge
129
+ * whose held-out loss rises by more than `tolerance` is refused and the base is
130
+ * returned unchanged. `score` maps a checkpoint to its held-out loss (lower is
131
+ * better); for an LM that is `meanEvalLoss` over the reloaded model, for a trained
132
+ * ML head its task's eval loss. Still the ONE merge — this only gates it.
133
+ */
134
+ export function mergeCheckpointDiffsGated(
135
+ baseCheckpoint: ArrayBuffer,
136
+ diffs: ArrayBuffer[],
137
+ weights: number[] | undefined,
138
+ score: (checkpoint: ArrayBuffer) => number | null,
139
+ opts: MergeGateOptions = {},
140
+ ): GatedMergeResult {
141
+ const merged = mergeCheckpointDiffs(baseCheckpoint, diffs, weights);
142
+ const refuse = (refusedReason: GatedMergeResult["refusedReason"], baseLoss: number | null, newLoss: number | null): GatedMergeResult => ({
143
+ ...merged,
144
+ checkpoint: baseCheckpoint,
145
+ deltaNorm: 0,
146
+ accepted: false,
147
+ baseLoss,
148
+ newLoss,
149
+ refusedReason,
150
+ });
151
+ if (merged.mergedRows === 0) return refuse("nothing-merged", null, null);
152
+ const baseLoss = score(baseCheckpoint);
153
+ const newLoss = score(merged.checkpoint);
154
+ if (baseLoss == null || newLoss == null) {
155
+ return opts.requireEval ? refuse("unscored", baseLoss, newLoss) : { ...merged, accepted: true, baseLoss, newLoss };
156
+ }
157
+ if (newLoss > baseLoss + (opts.tolerance ?? 0)) return refuse("regressed", baseLoss, newLoss);
158
+ return { ...merged, accepted: true, baseLoss, newLoss };
159
+ }