@seanhogg/builderforce-memory-engine 2026.10.1 → 2026.10.3

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 (149) hide show
  1. package/dist/evermind/adapt.d.ts +7 -0
  2. package/dist/evermind/adapt.d.ts.map +1 -1
  3. package/dist/evermind/adapt.js +1 -1
  4. package/dist/evermind/adapt.js.map +1 -1
  5. package/dist/evermind/delta_wire.d.ts +16 -1
  6. package/dist/evermind/delta_wire.d.ts.map +1 -1
  7. package/dist/evermind/delta_wire.js +22 -2
  8. package/dist/evermind/delta_wire.js.map +1 -1
  9. package/dist/evermind/index.d.ts +2 -2
  10. package/dist/evermind/index.d.ts.map +1 -1
  11. package/dist/evermind/index.js +1 -1
  12. package/dist/evermind/index.js.map +1 -1
  13. package/dist/evermind/merge.d.ts +5 -2
  14. package/dist/evermind/merge.d.ts.map +1 -1
  15. package/dist/evermind/merge.js +5 -2
  16. package/dist/evermind/merge.js.map +1 -1
  17. package/dist/evermind/ml_head.d.ts +28 -0
  18. package/dist/evermind/ml_head.d.ts.map +1 -0
  19. package/dist/evermind/ml_head.js +47 -0
  20. package/dist/evermind/ml_head.js.map +1 -0
  21. package/dist/evermind/package.d.ts +2 -2
  22. package/dist/evermind/package.d.ts.map +1 -1
  23. package/dist/evermind/package.js +1 -1
  24. package/dist/evermind/package.js.map +1 -1
  25. package/dist/experience/index.d.ts +2 -2
  26. package/dist/experience/index.d.ts.map +1 -1
  27. package/dist/experience/index.js.map +1 -1
  28. package/dist/experience/store.d.ts +19 -1
  29. package/dist/experience/store.d.ts.map +1 -1
  30. package/dist/experience/store.js +14 -2
  31. package/dist/experience/store.js.map +1 -1
  32. package/dist/experience/types.d.ts +18 -0
  33. package/dist/experience/types.d.ts.map +1 -1
  34. package/dist/experience/types.js.map +1 -1
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/kernels/weight_update.d.ts.map +1 -1
  40. package/dist/kernels/weight_update.js +12 -0
  41. package/dist/kernels/weight_update.js.map +1 -1
  42. package/dist/limbic/limbic_model.d.ts.map +1 -1
  43. package/dist/limbic/limbic_model.js +1 -3
  44. package/dist/limbic/limbic_model.js.map +1 -1
  45. package/dist/ml/classical/anomaly_composite.d.ts +32 -0
  46. package/dist/ml/classical/anomaly_composite.d.ts.map +1 -0
  47. package/dist/ml/classical/anomaly_composite.js +29 -0
  48. package/dist/ml/classical/anomaly_composite.js.map +1 -0
  49. package/dist/ml/classical/ewma.d.ts +7 -1
  50. package/dist/ml/classical/ewma.d.ts.map +1 -1
  51. package/dist/ml/classical/ewma.js +6 -4
  52. package/dist/ml/classical/ewma.js.map +1 -1
  53. package/dist/ml/classical/gbm.d.ts.map +1 -1
  54. package/dist/ml/classical/gbm.js +2 -1
  55. package/dist/ml/classical/gbm.js.map +1 -1
  56. package/dist/ml/classical/index.d.ts +6 -2
  57. package/dist/ml/classical/index.d.ts.map +1 -1
  58. package/dist/ml/classical/index.js +5 -1
  59. package/dist/ml/classical/index.js.map +1 -1
  60. package/dist/ml/classical/kmeans.d.ts.map +1 -1
  61. package/dist/ml/classical/kmeans.js +23 -4
  62. package/dist/ml/classical/kmeans.js.map +1 -1
  63. package/dist/ml/classical/knn.d.ts +15 -5
  64. package/dist/ml/classical/knn.d.ts.map +1 -1
  65. package/dist/ml/classical/knn.js +28 -7
  66. package/dist/ml/classical/knn.js.map +1 -1
  67. package/dist/ml/classical/logistic.d.ts +1 -9
  68. package/dist/ml/classical/logistic.d.ts.map +1 -1
  69. package/dist/ml/classical/logistic.js +17 -9
  70. package/dist/ml/classical/logistic.js.map +1 -1
  71. package/dist/ml/classical/naive_bayes.d.ts +5 -1
  72. package/dist/ml/classical/naive_bayes.d.ts.map +1 -1
  73. package/dist/ml/classical/naive_bayes.js +8 -3
  74. package/dist/ml/classical/naive_bayes.js.map +1 -1
  75. package/dist/ml/classical/robust_z.d.ts +15 -1
  76. package/dist/ml/classical/robust_z.d.ts.map +1 -1
  77. package/dist/ml/classical/robust_z.js +6 -1
  78. package/dist/ml/classical/robust_z.js.map +1 -1
  79. package/dist/ml/eval_loss.d.ts +15 -0
  80. package/dist/ml/eval_loss.d.ts.map +1 -0
  81. package/dist/ml/eval_loss.js +20 -0
  82. package/dist/ml/eval_loss.js.map +1 -0
  83. package/dist/ml/federate.d.ts +11 -25
  84. package/dist/ml/federate.d.ts.map +1 -1
  85. package/dist/ml/federate.js +15 -38
  86. package/dist/ml/federate.js.map +1 -1
  87. package/dist/ml/head_trainer.d.ts +16 -2
  88. package/dist/ml/head_trainer.d.ts.map +1 -1
  89. package/dist/ml/head_trainer.js +64 -27
  90. package/dist/ml/head_trainer.js.map +1 -1
  91. package/dist/ml/run.d.ts +11 -2
  92. package/dist/ml/run.d.ts.map +1 -1
  93. package/dist/ml/run.js +56 -17
  94. package/dist/ml/run.js.map +1 -1
  95. package/dist/ml/tasks/experience.d.ts +95 -13
  96. package/dist/ml/tasks/experience.d.ts.map +1 -1
  97. package/dist/ml/tasks/experience.js +172 -20
  98. package/dist/ml/tasks/experience.js.map +1 -1
  99. package/dist/ml/tasks/run_anomaly.d.ts +37 -19
  100. package/dist/ml/tasks/run_anomaly.d.ts.map +1 -1
  101. package/dist/ml/tasks/run_anomaly.js +50 -31
  102. package/dist/ml/tasks/run_anomaly.js.map +1 -1
  103. package/dist/ml/train.d.ts +8 -7
  104. package/dist/ml/train.d.ts.map +1 -1
  105. package/dist/ml/train.js +21 -30
  106. package/dist/ml/train.js.map +1 -1
  107. package/dist/ml/types.d.ts +6 -3
  108. package/dist/ml/types.d.ts.map +1 -1
  109. package/dist/ml/types.js +3 -2
  110. package/dist/ml/types.js.map +1 -1
  111. package/dist/optim/adamw.d.ts +11 -5
  112. package/dist/optim/adamw.d.ts.map +1 -1
  113. package/dist/optim/adamw.js +19 -4
  114. package/dist/optim/adamw.js.map +1 -1
  115. package/package.json +1 -1
  116. package/src/evermind/adapt.ts +8 -2
  117. package/src/evermind/delta_wire.ts +173 -148
  118. package/src/evermind/index.ts +36 -35
  119. package/src/evermind/merge.ts +10 -4
  120. package/src/evermind/ml_head.ts +63 -0
  121. package/src/evermind/package.ts +488 -488
  122. package/src/experience/index.ts +2 -1
  123. package/src/experience/store.ts +35 -3
  124. package/src/experience/types.ts +19 -0
  125. package/src/index.ts +2 -0
  126. package/src/kernels/weight_update.ts +12 -0
  127. package/src/limbic/limbic_model.ts +1 -4
  128. package/src/limbic/limbic_trainer.ts +91 -91
  129. package/src/ml/classical/anomaly_composite.ts +46 -0
  130. package/src/ml/classical/ewma.ts +8 -2
  131. package/src/ml/classical/gbm.ts +144 -143
  132. package/src/ml/classical/index.ts +8 -2
  133. package/src/ml/classical/kmeans.ts +22 -4
  134. package/src/ml/classical/knn.ts +31 -8
  135. package/src/ml/classical/logistic.ts +9 -9
  136. package/src/ml/classical/math.ts +93 -93
  137. package/src/ml/classical/naive_bayes.ts +10 -3
  138. package/src/ml/classical/robust_z.ts +21 -2
  139. package/src/ml/eval_loss.ts +27 -0
  140. package/src/ml/federate.ts +22 -52
  141. package/src/ml/head_trainer.ts +66 -48
  142. package/src/ml/registry.ts +44 -44
  143. package/src/ml/run.ts +57 -13
  144. package/src/ml/tasks/experience.ts +222 -23
  145. package/src/ml/tasks/run_anomaly.ts +62 -37
  146. package/src/ml/train.ts +34 -31
  147. package/src/ml/types.ts +135 -132
  148. package/src/optim/adamw.ts +21 -5
  149. package/src/training/cpu_ops.ts +141 -141
@@ -1,148 +1,173 @@
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
+ /**
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
+ * - which checkpoint a door merges into: a parse names its door's target (the model
12
+ * by default), so an ML-head diff can never reach the model's own merge — it would
13
+ * pass every structural check and overwrite the first n floats of the LM weights;
14
+ * - the size cap, so a producer can fall back before it is refused;
15
+ * - the structural check a delta must pass before it may join a merge batch.
16
+ *
17
+ * That last one is load-bearing: `mergeCheckpointDiffs` throws on a malformed diff,
18
+ * and one throw drops the whole batch. A single bad push must cost only itself.
19
+ */
20
+ import { deserializeRowDelta } from "../utils/delta.js";
21
+ import { verifyCrcTrailer } from "../utils/crc32.js";
22
+ import { base64ToBytes, bytesToBase64 } from "../utils/base64.js";
23
+ import { ML_TASK_ID_RE } from "./ml_head.js";
24
+
25
+ /** Max accepted base64 delta, in characters (~8 MiB). */
26
+ export const MAX_DELTA_B64_CHARS = 8 * 1024 * 1024;
27
+
28
+ /** Chars of provenance label kept — a diff carries no text, so the label is its only description. */
29
+ export const DELTA_LABEL_MAX_CHARS = 800;
30
+
31
+ /** A delta's target: absent = the model's own checkpoint; `ml:<task id>` = that trained ML head. */
32
+ export const DELTA_TARGET_RE = new RegExp(`^ml:${ML_TASK_ID_RE.source.slice(1)}`);
33
+
34
+ /** The delta target of the trained head for catalog task `taskId`. */
35
+ export function mlHeadTarget(taskId: string): string {
36
+ return `ml:${taskId}`;
37
+ }
38
+
39
+ /**
40
+ * Why a delta's `target` does not belong at a door that merges into `expected`
41
+ * (undefined = the model's own checkpoint), or null when it does. The ONE target
42
+ * check: the parse and the ML-head merge both use it.
43
+ */
44
+ export function deltaTargetMismatch(target: string | undefined, expected: string | undefined): string | null {
45
+ if (target === expected) return null;
46
+ return `target ${target ?? "(model)"} does not belong here; this door merges into ${expected ?? "the model checkpoint"}`;
47
+ }
48
+
49
+ /** Standard base64 alphabet with optional padding. */
50
+ const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
51
+
52
+ /** A validated delta contribution — the exact body a producer sends. */
53
+ export interface DeltaLearnPayload {
54
+ /** base64 serialized RowDelta (`diffCheckpoints(base, adapted)`). */
55
+ diff: string;
56
+ /** The model version the delta was diffed against. */
57
+ baseVersion: number;
58
+ /** FedAvg sample weight (> 0). Absent means the writer's default (1). */
59
+ weight?: number;
60
+ /** Provenance for the merged row (e.g. the run's task). */
61
+ label?: string;
62
+ /**
63
+ * Which checkpoint the diff is against. Absent = the model's own weights (every
64
+ * delta written before heads federated); `ml:<task id>` = that trained ML head.
65
+ */
66
+ target?: string;
67
+ }
68
+
69
+ /** Why a body was refused: `invalid` (malformed) or `too-large` (over {@link MAX_DELTA_B64_CHARS}). */
70
+ export type DeltaParseResult =
71
+ | { ok: true; payload: DeltaLearnPayload }
72
+ | { ok: false; reason: "invalid" | "too-large"; error: string };
73
+
74
+ /** Which checkpoint the door calling {@link parseDeltaLearnPayload} merges into. */
75
+ export interface DeltaParseOptions {
76
+ /** Absent = the model's own checkpoint (the LM door); `ml:<task id>` = that head's door. */
77
+ target?: string;
78
+ }
79
+
80
+ /**
81
+ * Parse + validate a delta body. Pure — a front door calls it to refuse a bad body
82
+ * early, and the single writer calls it again because it trusts no caller.
83
+ *
84
+ * A body whose `target` is not the door's (`opts.target`; absent = the model) is
85
+ * refused as `invalid`: the LM door never admits an ML-head delta, and a head's door
86
+ * never admits a model delta.
87
+ */
88
+ export function parseDeltaLearnPayload(raw: unknown, opts: DeltaParseOptions = {}): DeltaParseResult {
89
+ const body = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
90
+ const diff = typeof body.diff === "string" ? body.diff : "";
91
+ const baseVersion = typeof body.baseVersion === "number" ? body.baseVersion : Number.NaN;
92
+ if (!diff || !Number.isInteger(baseVersion)) {
93
+ return {
94
+ ok: false,
95
+ reason: "invalid",
96
+ error: "diff (base64) and baseVersion (integer head version the delta was taken against) are required",
97
+ };
98
+ }
99
+ if (diff.length > MAX_DELTA_B64_CHARS) {
100
+ return { ok: false, reason: "too-large", error: `delta too large (max ${MAX_DELTA_B64_CHARS} base64 characters)` };
101
+ }
102
+ if (diff.length % 4 !== 0 || !BASE64_RE.test(diff)) {
103
+ return { ok: false, reason: "invalid", error: "diff must be standard base64" };
104
+ }
105
+ const weight = typeof body.weight === "number" && Number.isFinite(body.weight) && body.weight > 0 ? body.weight : undefined;
106
+ const label = typeof body.label === "string" && body.label.trim() ? body.label.trim().slice(0, DELTA_LABEL_MAX_CHARS) : undefined;
107
+ if (body.target !== undefined && (typeof body.target !== "string" || !DELTA_TARGET_RE.test(body.target))) {
108
+ return { ok: false, reason: "invalid", error: "target, when present, must be ml:<task id>" };
109
+ }
110
+ const target = body.target as string | undefined;
111
+ const mismatch = deltaTargetMismatch(target, opts.target);
112
+ if (mismatch) return { ok: false, reason: "invalid", error: mismatch };
113
+ return {
114
+ ok: true,
115
+ payload: {
116
+ diff,
117
+ baseVersion,
118
+ ...(weight !== undefined ? { weight } : {}),
119
+ ...(label ? { label } : {}),
120
+ ...(target ? { target } : {}),
121
+ },
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Build the payload for one computed diff. The caller owns the size guard (compare
127
+ * `payload.diff.length` with {@link MAX_DELTA_B64_CHARS}) so it can choose a fallback.
128
+ */
129
+ export function buildDeltaLearnPayload(
130
+ diff: ArrayBuffer,
131
+ baseVersion: number,
132
+ weight?: number,
133
+ label?: string,
134
+ target?: string,
135
+ ): DeltaLearnPayload {
136
+ const trimmed = (label ?? "").trim();
137
+ if (target !== undefined && !DELTA_TARGET_RE.test(target)) throw new Error(`buildDeltaLearnPayload: bad target '${target}'`);
138
+ return {
139
+ diff: bytesToBase64(diff),
140
+ baseVersion,
141
+ ...(typeof weight === "number" && Number.isFinite(weight) && weight > 0 ? { weight } : {}),
142
+ ...(trimmed ? { label: trimmed.slice(0, DELTA_LABEL_MAX_CHARS) } : {}),
143
+ ...(target ? { target } : {}),
144
+ };
145
+ }
146
+
147
+ /** Decode a payload's base64 `diff` to the serialized RowDelta bytes. Throws on malformed base64. */
148
+ export function decodeDeltaPayload(payload: Pick<DeltaLearnPayload, "diff">): ArrayBuffer {
149
+ return base64ToBytes(payload.diff);
150
+ }
151
+
152
+ /**
153
+ * Structural check of ONE delta against the base it claims to diff: it must
154
+ * deserialize, be element-granular, index only elements the base has, and carry
155
+ * finite values. Returns why it is unusable, or null when it can join a merge.
156
+ */
157
+ export function deltaUnusableReason(delta: ArrayBuffer, baseCheckpoint: ArrayBuffer): string | null {
158
+ try {
159
+ const rd = deserializeRowDelta(delta);
160
+ if (rd.rowSize !== 1) return `expected an element-granular delta (rowSize 1), got ${rd.rowSize}`;
161
+ const elements = verifyCrcTrailer(baseCheckpoint).body.byteLength / 4;
162
+ for (let i = 0; i < rd.rows.length; i++) {
163
+ const idx = rd.rows[i]!;
164
+ if (!Number.isInteger(idx) || idx < 0 || idx >= elements) {
165
+ return `row index ${idx} is outside the base checkpoint (${elements} elements)`;
166
+ }
167
+ if (!Number.isFinite(rd.data[i]!)) return `non-finite value at row ${idx}`;
168
+ }
169
+ return null;
170
+ } catch (error) {
171
+ return `not a serialized RowDelta: ${error instanceof Error ? error.message : String(error)}`;
172
+ }
173
+ }
@@ -1,35 +1,36 @@
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";
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
+ deltaTargetMismatch,
26
+ MAX_DELTA_B64_CHARS,
27
+ DELTA_TARGET_RE,
28
+ mlHeadTarget,
29
+ } from "./delta_wire.js";
30
+ export type { DeltaLearnPayload, DeltaParseResult, DeltaParseOptions } from "./delta_wire.js";
31
+
32
+ export { mergeCheckpointDiffs, mergeCheckpointDiffsGated } from "./merge.js";
33
+ export type { MergeResult, GatedMergeResult, MergeGateOptions } from "./merge.js";
34
+
35
+ export { meanEvalLoss } from "./eval.js";
36
+ export type { EvalExample, EvalTokenizer } from "./eval.js";
@@ -109,7 +109,7 @@ export function mergeCheckpointDiffs(
109
109
  export interface GatedMergeResult extends MergeResult {
110
110
  /** True when the merged checkpoint was kept. When false, `checkpoint` is the BASE. */
111
111
  accepted: boolean;
112
- /** Held-out loss of the base / the merged candidate (null when unscorable). */
112
+ /** Held-out loss of the base / the merged candidate (null when unscorable, including a non-finite loss). */
113
113
  baseLoss: number | null;
114
114
  newLoss: number | null;
115
115
  /** Why the merge was refused. */
@@ -119,7 +119,10 @@ export interface GatedMergeResult extends MergeResult {
119
119
  export interface MergeGateOptions {
120
120
  /** Loss increase still accepted (absolute). Default 0 — any regression refuses. */
121
121
  tolerance?: number;
122
- /** Refuse when either side cannot be scored. Default false (an unscored merge is kept). */
122
+ /**
123
+ * Refuse when either side cannot be scored (null, NaN or ±Infinity). Default false
124
+ * here (an unscored LM merge is kept); `mergeMlHeadDeltas` defaults it to true.
125
+ */
123
126
  requireEval?: boolean;
124
127
  }
125
128
 
@@ -149,8 +152,11 @@ export function mergeCheckpointDiffsGated(
149
152
  refusedReason,
150
153
  });
151
154
  if (merged.mergedRows === 0) return refuse("nothing-merged", null, null);
152
- const baseLoss = score(baseCheckpoint);
153
- const newLoss = score(merged.checkpoint);
155
+ // A non-finite loss is no measurement: NaN compares false against everything, so
156
+ // left as a number it would wave a diverged candidate straight through the gate.
157
+ const finiteOrNull = (l: number | null): number | null => (l != null && Number.isFinite(l) ? l : null);
158
+ const baseLoss = finiteOrNull(score(baseCheckpoint));
159
+ const newLoss = finiteOrNull(score(merged.checkpoint));
154
160
  if (baseLoss == null || newLoss == null) {
155
161
  return opts.requireEval ? refuse("unscored", baseLoss, newLoss) : { ...merged, accepted: true, baseLoss, newLoss };
156
162
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * evermind/ml_head.ts — what the `.evermind` artifact needs to know about a trained
3
+ * ML-catalog head: its manifest entry shape, its checkpoint format and its structural
4
+ * check, plus the task-id grammar the delta wire's `ml:<task>` target uses.
5
+ *
6
+ * A leaf: it imports only utils/. `evermind/package.ts` and `evermind/delta_wire.ts`
7
+ * import it, and so does `ml/` (which re-exports it) — so the dependency runs one way,
8
+ * ml → evermind, never back.
9
+ */
10
+ import { base64ToBytes } from "../utils/base64.js";
11
+ import { appendCrcTrailer, verifyCrcTrailer } from "../utils/crc32.js";
12
+
13
+ /** Task ids: lowercase dotted segments, e.g. `run.local-success`. */
14
+ export const ML_TASK_ID_RE = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/;
15
+
16
+ /**
17
+ * One trained ML-catalog head carried in a model's manifest (`mlHeads`): small, so
18
+ * it rides in the manifest, and versioned, so it federates — a delta against it is
19
+ * `target: "ml:<task>"`, `baseVersion: version` on the delta wire.
20
+ */
21
+ export interface EvermindMlHead {
22
+ /** Catalog task id, e.g. `run.local-success`. */
23
+ task: string;
24
+ /** Engine key, e.g. `local:classical:logistic`. */
25
+ engine: string;
26
+ /** Integer head version — what a federated delta for it is diffed against. */
27
+ version: number;
28
+ /** What a loader needs to rebuild the predictor (e.g. `{ dim, classes }`). */
29
+ shape: Record<string, number>;
30
+ /** base64 of the CRC-trailed f32 checkpoint of the head's flat parameters. */
31
+ params: string;
32
+ /** Held-out loss of this version, when measured. */
33
+ evalLoss?: number;
34
+ }
35
+
36
+ /** A head's flat parameters as a CRC-trailed f32 checkpoint. */
37
+ export function headCheckpoint(params: ArrayLike<number>): ArrayBuffer {
38
+ const body = new ArrayBuffer(params.length * 4);
39
+ new Float32Array(body).set(Array.from(params));
40
+ return appendCrcTrailer(body);
41
+ }
42
+
43
+ /** The flat parameters of a head checkpoint. Throws on a failed CRC. */
44
+ export function headParams(checkpoint: ArrayBuffer): Float32Array {
45
+ const crc = verifyCrcTrailer(checkpoint);
46
+ if (!crc.ok) throw new Error("headParams: checkpoint failed its CRC check");
47
+ if (crc.body.byteLength % 4 !== 0) throw new Error("headParams: checkpoint is not f32");
48
+ return new Float32Array(crc.body);
49
+ }
50
+
51
+ /** Why a manifest head entry is unusable, or null (the package's validate() calls this). */
52
+ export function mlHeadUnusableReason(h: EvermindMlHead): string | null {
53
+ if (!h || typeof h.task !== "string" || typeof h.engine !== "string") return "task and engine are required";
54
+ if (!Number.isInteger(h.version) || h.version < 0) return "version must be a non-negative integer";
55
+ try {
56
+ const crc = verifyCrcTrailer(base64ToBytes(h.params));
57
+ if (!crc.hasTrailer || !crc.ok) return "params checkpoint failed its CRC check";
58
+ if (crc.body.byteLength % 4 !== 0) return "params checkpoint is not f32";
59
+ } catch {
60
+ return "params is not base64";
61
+ }
62
+ return null;
63
+ }