@evomap/evolver-core 2.0.0-beta.1 → 2.0.0-beta.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 (110) hide show
  1. package/assets/gep/genes.jsonl +5 -0
  2. package/dist/algo/candidateAssembly.js +23 -14
  3. package/dist/algo/capabilityCandidates.d.ts +2 -0
  4. package/dist/algo/capabilityCandidates.js +5 -0
  5. package/dist/algo/conversationSniffer.d.ts +18 -0
  6. package/dist/algo/conversationSniffer.js +132 -0
  7. package/dist/algo/cycleEngine.d.ts +11 -0
  8. package/dist/algo/cycleEngine.js +14 -6
  9. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  10. package/dist/algo/cycleFailureClassifier.js +13 -5
  11. package/dist/algo/geneIntake.d.ts +14 -4
  12. package/dist/algo/geneIntake.js +37 -9
  13. package/dist/algo/geneSelection.d.ts +18 -1
  14. package/dist/algo/geneSelection.js +42 -18
  15. package/dist/algo/index.d.ts +2 -0
  16. package/dist/algo/index.js +2 -0
  17. package/dist/algo/memoryGraph.d.ts +62 -0
  18. package/dist/algo/memoryGraph.js +86 -0
  19. package/dist/algo/orchestrator.d.ts +3 -0
  20. package/dist/algo/orchestrator.js +14 -2
  21. package/dist/assetstore/assetSidecarRecords.d.ts +23 -0
  22. package/dist/assetstore/assetSidecarRecords.js +142 -0
  23. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  24. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  25. package/dist/assetstore/assetStoreHealth.d.ts +75 -0
  26. package/dist/assetstore/assetStoreHealth.js +277 -0
  27. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  28. package/dist/assetstore/assetStoreLayout.js +6 -0
  29. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  30. package/dist/assetstore/assetStoreStorage.js +318 -0
  31. package/dist/assetstore/assetSyncLedger.d.ts +32 -0
  32. package/dist/assetstore/assetSyncLedger.js +66 -0
  33. package/dist/assetstore/index.d.ts +5 -1
  34. package/dist/assetstore/index.js +5 -1
  35. package/dist/assetstore/localJsonl.d.ts +10 -2
  36. package/dist/assetstore/localJsonl.js +93 -37
  37. package/dist/assetstore/pendingSignals.js +3 -1
  38. package/dist/assetstore/provenance.d.ts +20 -2
  39. package/dist/assetstore/provenance.js +92 -56
  40. package/dist/assetstore/provider.d.ts +2 -0
  41. package/dist/assetstore/reviewFilter.js +5 -2
  42. package/dist/assetstore/reviewLedger.d.ts +14 -2
  43. package/dist/assetstore/reviewLedger.js +78 -43
  44. package/dist/assetstore/seedGenes.d.ts +3 -0
  45. package/dist/assetstore/seedGenes.js +134 -0
  46. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  47. package/dist/benchmark/antiGeneBenchmark.js +11 -1
  48. package/dist/benchmark/antiGeneImpact.d.ts +16 -0
  49. package/dist/benchmark/antiGeneImpact.js +34 -0
  50. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  51. package/dist/benchmark/antiGeneRollout.js +13 -1
  52. package/dist/benchmark/index.d.ts +2 -1
  53. package/dist/benchmark/index.js +2 -1
  54. package/dist/benchmark/triggerShift.d.ts +62 -0
  55. package/dist/benchmark/triggerShift.js +106 -0
  56. package/dist/events/eventArchive.d.ts +65 -0
  57. package/dist/events/eventArchive.js +343 -0
  58. package/dist/events/eventStore.js +2 -12
  59. package/dist/events/ingest.d.ts +1 -1
  60. package/dist/events/ingest.js +2 -0
  61. package/dist/events/paths.d.ts +1 -1
  62. package/dist/events/paths.js +2 -2
  63. package/dist/events/public.d.ts +3 -1
  64. package/dist/events/public.js +2 -1
  65. package/dist/events/retention.d.ts +24 -1
  66. package/dist/events/retention.js +133 -37
  67. package/dist/exec/autoExec.d.ts +6 -1
  68. package/dist/exec/autoExec.js +34 -0
  69. package/dist/exec/autonomousCycle.d.ts +2 -0
  70. package/dist/exec/autonomousCycle.js +5 -0
  71. package/dist/exec/claudeBridge.d.ts +7 -2
  72. package/dist/exec/claudeBridge.js +115 -14
  73. package/dist/exec/prompt.js +9 -0
  74. package/dist/exec/runnerRegistry.d.ts +64 -18
  75. package/dist/exec/runnerRegistry.js +341 -38
  76. package/dist/exec/selfPrObfuscation.d.ts +2 -1
  77. package/dist/exec/selfPrObfuscation.js +19 -6
  78. package/dist/hub/agentDirectory.d.ts +90 -0
  79. package/dist/hub/agentDirectory.js +104 -0
  80. package/dist/hub/bindings.js +23 -3
  81. package/dist/hub/capability.d.ts +14 -2
  82. package/dist/hub/fake.d.ts +4 -2
  83. package/dist/hub/fake.js +3 -2
  84. package/dist/hub/index.d.ts +1 -0
  85. package/dist/hub/index.js +1 -0
  86. package/dist/mailbox/catalog.js +3 -0
  87. package/dist/mailbox/ipcServer.js +2 -2
  88. package/dist/mailbox/store.d.ts +14 -0
  89. package/dist/mailbox/store.js +44 -2
  90. package/dist/material/consumer.js +9 -6
  91. package/dist/material/index.d.ts +1 -0
  92. package/dist/material/index.js +1 -0
  93. package/dist/material/materialArchive.d.ts +81 -0
  94. package/dist/material/materialArchive.js +466 -0
  95. package/dist/material/materialStore.d.ts +1 -0
  96. package/dist/material/materialStore.js +6 -13
  97. package/dist/ops/savingsCore.js +1 -2
  98. package/dist/ops/selfUpdate.d.ts +10 -1
  99. package/dist/ops/selfUpdate.js +64 -15
  100. package/dist/schema/common.d.ts +1 -1
  101. package/dist/schema/common.js +1 -1
  102. package/dist/schema/material.d.ts +5 -5
  103. package/dist/trace/trajectoryExport.js +2 -2
  104. package/dist/util/fileLock.d.ts +19 -2
  105. package/dist/util/fileLock.js +166 -31
  106. package/dist/wire/geneHints.d.ts +42 -1
  107. package/dist/wire/geneHints.js +52 -1
  108. package/dist/wire/index.d.ts +14 -2
  109. package/dist/wire/index.js +1 -1
  110. package/package.json +6 -1
@@ -0,0 +1,466 @@
1
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeSync, } from 'node:fs';
2
+ import { basename, dirname, extname, join } from 'node:path';
3
+ import { material } from '../schema/material.js';
4
+ import { acquireLock, releaseLock } from '../util/fileLock.js';
5
+ export const MATERIAL_ARCHIVE_DEFAULT_KEEP_RECORDS = 1_000;
6
+ const SEGMENT_PATTERN = /^material-(\d{16})-(\d{16})\.jsonl$/;
7
+ export class InvalidMaterialLogError extends Error {
8
+ line;
9
+ code = 'MATERIAL_ARCHIVE_INVALID_LOG';
10
+ constructor(line, reason) {
11
+ super(`material archive refused invalid active log at line ${line}: ${reason}`);
12
+ this.line = line;
13
+ this.name = 'InvalidMaterialLogError';
14
+ }
15
+ }
16
+ export class InvalidMaterialArchiveError extends Error {
17
+ segment;
18
+ line;
19
+ code = 'MATERIAL_ARCHIVE_INVALID_ARCHIVE';
20
+ constructor(segment, line, reason) {
21
+ super(`material archive segment ${segment} is invalid at line ${line}: ${reason}`);
22
+ this.segment = segment;
23
+ this.line = line;
24
+ this.name = 'InvalidMaterialArchiveError';
25
+ }
26
+ }
27
+ export class MaterialArchiveRangeError extends Error {
28
+ code = 'MATERIAL_ARCHIVE_RANGE_INVALID';
29
+ constructor(reason) {
30
+ super(`material archive range is invalid: ${reason}`);
31
+ this.name = 'MaterialArchiveRangeError';
32
+ }
33
+ }
34
+ export class MaterialArchiveSegmentConflictError extends Error {
35
+ archiveId;
36
+ code = 'MATERIAL_ARCHIVE_SEGMENT_CONFLICT';
37
+ constructor(archiveId) {
38
+ super(`material archive segment ${archiveId} already exists with different content`);
39
+ this.archiveId = archiveId;
40
+ this.name = 'MaterialArchiveSegmentConflictError';
41
+ }
42
+ }
43
+ export class InvalidMaterialArchiveCursorError extends Error {
44
+ code = 'MATERIAL_ARCHIVE_CURSOR_INVALID';
45
+ constructor(reason) {
46
+ super(`material archive cursor is invalid: ${reason}`);
47
+ this.name = 'InvalidMaterialArchiveCursorError';
48
+ }
49
+ }
50
+ export function materialArchiveDir(path) {
51
+ const ext = extname(path);
52
+ const stem = ext.length > 0 ? basename(path, ext) : basename(path);
53
+ return join(dirname(path), `${stem}.archive`);
54
+ }
55
+ export function materialArchiveSegmentName(start, end) {
56
+ return `material-${String(start).padStart(16, '0')}-${String(end).padStart(16, '0')}.jsonl`;
57
+ }
58
+ export function planMaterialArchive(opts) {
59
+ const keepRecords = normalizeKeepRecords(opts.keepRecords);
60
+ const state = readStrictHistory(opts.path);
61
+ const historyRecords = state.archive.length + state.logicalActive.length;
62
+ const cursors = readCursorsStrict(opts.cursorPaths ?? [], historyRecords);
63
+ const minCursor = minimumCursor(cursors, state.archive.length);
64
+ const wouldArchive = archiveableCount(state, minCursor, keepRecords);
65
+ const start = state.archive.length;
66
+ const end = start + wouldArchive - 1;
67
+ return {
68
+ mode: 'preview',
69
+ activeRecords: state.logicalActive.length,
70
+ physicalActiveRecords: state.active.length,
71
+ archiveRecords: state.archive.length,
72
+ historyRecords,
73
+ keepRecords,
74
+ minCursor,
75
+ cursors,
76
+ overlapRecords: state.overlap,
77
+ wouldArchive,
78
+ retainedRecords: state.logicalActive.length - wouldArchive,
79
+ archiveId: wouldArchive === 0 ? null : `${start}-${end}`,
80
+ };
81
+ }
82
+ export function archiveMaterialStore(opts) {
83
+ const keepRecords = normalizeKeepRecords(opts.keepRecords);
84
+ const lockPath = `${opts.path}.lock`;
85
+ mkdirSync(dirname(opts.path), { recursive: true });
86
+ acquireLock(lockPath);
87
+ try {
88
+ const state = readStrictHistory(opts.path);
89
+ const historyRecords = state.archive.length + state.logicalActive.length;
90
+ const cursors = readCursorsStrict(opts.cursorPaths ?? [], historyRecords);
91
+ const minCursor = minimumCursor(cursors, state.archive.length);
92
+ const archivedRecords = archiveableCount(state, minCursor, keepRecords);
93
+ const start = state.archive.length;
94
+ const end = start + archivedRecords - 1;
95
+ const archiveId = archivedRecords === 0 ? null : `${start}-${end}`;
96
+ if (archivedRecords > 0) {
97
+ const prefix = state.logicalActive.slice(0, archivedRecords);
98
+ const archiveDir = materialArchiveDir(opts.path);
99
+ mkdirSync(archiveDir, { recursive: true });
100
+ const segmentPath = join(archiveDir, materialArchiveSegmentName(start, end));
101
+ const contents = serializeLines(prefix);
102
+ if (existsSync(segmentPath)) {
103
+ if (readFileSync(segmentPath, 'utf8') !== contents) {
104
+ throw new MaterialArchiveSegmentConflictError(archiveId ?? `${start}-${end}`);
105
+ }
106
+ }
107
+ else {
108
+ durableReplace(`${segmentPath}.tmp`, segmentPath, contents);
109
+ fsyncDirectoryBestEffort(archiveDir);
110
+ }
111
+ }
112
+ const tail = state.logicalActive.slice(archivedRecords);
113
+ if (archivedRecords > 0 || state.overlap > 0) {
114
+ durableReplace(`${opts.path}.archive.tmp`, opts.path, serializeLines(tail));
115
+ fsyncDirectoryBestEffort(dirname(opts.path));
116
+ }
117
+ return {
118
+ mode: 'write',
119
+ activeRecordsBefore: state.logicalActive.length,
120
+ physicalActiveRecordsBefore: state.active.length,
121
+ keepRecords,
122
+ minCursor,
123
+ cursors,
124
+ archivedRecords,
125
+ retainedRecords: tail.length,
126
+ archiveRecords: state.archive.length + archivedRecords,
127
+ historyRecords,
128
+ recoveredOverlap: state.overlap,
129
+ archiveId,
130
+ };
131
+ }
132
+ finally {
133
+ releaseLock(lockPath);
134
+ }
135
+ }
136
+ /** Operational, fail-soft full history reader. Strict archive writes validate every line and range. */
137
+ export function readMaterialHistory(path) {
138
+ // Snapshot active first. Writers persist an archive segment before replacing active, so every interleaving
139
+ // now observes either the old active state or a recoverable archive/active overlap, never a new tail with an
140
+ // old archive base.
141
+ const active = readMaterialsLenient(path);
142
+ const ordered = [];
143
+ const positions = new Map();
144
+ const archiveDir = materialArchiveDir(path);
145
+ for (const descriptor of segmentDescriptors(archiveDir)) {
146
+ for (const record of readMaterialsLenient(join(archiveDir, descriptor.file))) {
147
+ if (positions.has(record.materialId))
148
+ continue;
149
+ positions.set(record.materialId, ordered.length);
150
+ ordered.push(record);
151
+ }
152
+ }
153
+ // The active log is the current writer source. In a crash overlap it owns the duplicate record while its
154
+ // original absolute position remains stable.
155
+ for (const record of active) {
156
+ const position = positions.get(record.materialId);
157
+ if (position === undefined) {
158
+ positions.set(record.materialId, ordered.length);
159
+ ordered.push(record);
160
+ }
161
+ else {
162
+ ordered[position] = record;
163
+ }
164
+ }
165
+ return ordered;
166
+ }
167
+ /** Absolute-offset range reader used by durable consumers after active records move into archive segments. */
168
+ export function readMaterialRange(path, start, count) {
169
+ const normalizedStart = normalizeRangeStart(start);
170
+ const normalizedCount = normalizeRangeCount(count);
171
+ if (normalizedCount === 0)
172
+ return [];
173
+ // Keep the same active-first snapshot order as readMaterialHistory; see its concurrency note.
174
+ const active = readMaterialsLenient(path);
175
+ const archiveDir = materialArchiveDir(path);
176
+ const descriptors = segmentDescriptors(archiveDir);
177
+ if (descriptors.length === 0) {
178
+ return active.slice(normalizedStart, normalizedStart + normalizedCount);
179
+ }
180
+ const archiveEnd = Math.max(...descriptors.map((descriptor) => descriptor.end));
181
+ const requestedEnd = Math.min(Number.MAX_SAFE_INTEGER, normalizedStart + normalizedCount - 1);
182
+ const byOffset = new Map();
183
+ for (const descriptor of descriptors) {
184
+ if (descriptor.end < normalizedStart || descriptor.start > requestedEnd)
185
+ continue;
186
+ const records = readMaterialsLenient(join(archiveDir, descriptor.file));
187
+ for (let index = 0; index < records.length; index += 1) {
188
+ const offset = descriptor.start + index;
189
+ if (offset >= normalizedStart && offset <= requestedEnd && !byOffset.has(offset)) {
190
+ byOffset.set(offset, records[index]);
191
+ }
192
+ }
193
+ }
194
+ if (requestedEnd > archiveEnd) {
195
+ const overlap = detectOperationalOverlap(path, descriptors, active);
196
+ const logicalActive = active.slice(overlap);
197
+ const activeStart = archiveEnd + 1;
198
+ for (let index = 0; index < logicalActive.length; index += 1) {
199
+ const offset = activeStart + index;
200
+ if (offset >= normalizedStart && offset <= requestedEnd)
201
+ byOffset.set(offset, logicalActive[index]);
202
+ }
203
+ }
204
+ return [...byOffset.entries()]
205
+ .sort(([left], [right]) => left - right)
206
+ .map(([, record]) => record);
207
+ }
208
+ export function inspectMaterialArchive(path) {
209
+ const archiveDir = materialArchiveDir(path);
210
+ const descriptors = segmentDescriptors(archiveDir);
211
+ let bytes = 0;
212
+ let records = 0;
213
+ let invalidLines = 0;
214
+ for (const descriptor of descriptors) {
215
+ const segmentPath = join(archiveDir, descriptor.file);
216
+ bytes += statSync(segmentPath).size;
217
+ for (const line of readFileSync(segmentPath, 'utf8').split('\n')) {
218
+ if (line.length === 0)
219
+ continue;
220
+ try {
221
+ material.parse(JSON.parse(line));
222
+ records += 1;
223
+ }
224
+ catch {
225
+ invalidLines += 1;
226
+ }
227
+ }
228
+ }
229
+ return {
230
+ exists: descriptors.length > 0,
231
+ segments: descriptors.length,
232
+ bytes,
233
+ records,
234
+ invalidLines,
235
+ firstOffset: descriptors[0]?.start ?? null,
236
+ lastOffset: descriptors[descriptors.length - 1]?.end ?? null,
237
+ };
238
+ }
239
+ function readStrictHistory(path) {
240
+ const archive = readArchiveStrict(materialArchiveDir(path));
241
+ const active = readActiveStrict(path);
242
+ const overlap = detectOverlap(archive, active);
243
+ const logicalActive = active.slice(overlap);
244
+ const seen = new Map();
245
+ for (const line of [...archive, ...logicalActive]) {
246
+ const canonical = JSON.stringify(line.record);
247
+ const existing = seen.get(line.record.materialId);
248
+ if (existing !== undefined) {
249
+ if (existing !== canonical)
250
+ throw new MaterialArchiveSegmentConflictError(line.record.materialId);
251
+ throw new MaterialArchiveRangeError(`duplicate material ${line.record.materialId}`);
252
+ }
253
+ seen.set(line.record.materialId, canonical);
254
+ }
255
+ return { archive, active, logicalActive, overlap };
256
+ }
257
+ function readArchiveStrict(archiveDir) {
258
+ const descriptors = segmentDescriptors(archiveDir, true);
259
+ const out = [];
260
+ let expectedStart = 0;
261
+ for (const descriptor of descriptors) {
262
+ if (descriptor.start !== expectedStart) {
263
+ throw new MaterialArchiveRangeError(`expected offset ${expectedStart} but found ${descriptor.start}`);
264
+ }
265
+ const lines = readStrictLines(join(archiveDir, descriptor.file), descriptor.file, true);
266
+ const expectedLength = descriptor.end - descriptor.start + 1;
267
+ if (lines.length !== expectedLength) {
268
+ throw new MaterialArchiveRangeError(`${descriptor.file} declares ${expectedLength} record(s) but contains ${lines.length}`);
269
+ }
270
+ out.push(...lines);
271
+ expectedStart = descriptor.end + 1;
272
+ }
273
+ return out;
274
+ }
275
+ function readActiveStrict(path) {
276
+ if (!existsSync(path))
277
+ return [];
278
+ const out = [];
279
+ const lines = readFileSync(path, 'utf8').split('\n');
280
+ for (let index = 0; index < lines.length; index += 1) {
281
+ const raw = lines[index];
282
+ if (raw.length === 0)
283
+ continue;
284
+ try {
285
+ out.push({ record: material.parse(JSON.parse(raw)), raw });
286
+ }
287
+ catch {
288
+ throw new InvalidMaterialLogError(index + 1, 'invalid json or material schema');
289
+ }
290
+ }
291
+ return out;
292
+ }
293
+ function readStrictLines(path, segment, archive) {
294
+ const out = [];
295
+ const lines = readFileSync(path, 'utf8').split('\n');
296
+ for (let index = 0; index < lines.length; index += 1) {
297
+ const raw = lines[index];
298
+ if (raw.length === 0)
299
+ continue;
300
+ try {
301
+ out.push({ record: material.parse(JSON.parse(raw)), raw });
302
+ }
303
+ catch {
304
+ if (archive)
305
+ throw new InvalidMaterialArchiveError(segment, index + 1, 'invalid json or material schema');
306
+ throw new InvalidMaterialLogError(index + 1, 'invalid json or material schema');
307
+ }
308
+ }
309
+ return out;
310
+ }
311
+ function readMaterialsLenient(path) {
312
+ if (!existsSync(path))
313
+ return [];
314
+ const out = [];
315
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
316
+ if (line.length === 0)
317
+ continue;
318
+ try {
319
+ out.push(material.parse(JSON.parse(line)));
320
+ }
321
+ catch {
322
+ // Preserve MaterialStore's existing fail-soft reads. Archive writes use strict parsing.
323
+ }
324
+ }
325
+ return out;
326
+ }
327
+ function segmentDescriptors(archiveDir, strict = false) {
328
+ if (!existsSync(archiveDir))
329
+ return [];
330
+ const out = [];
331
+ for (const file of readdirSync(archiveDir).sort()) {
332
+ const match = SEGMENT_PATTERN.exec(file);
333
+ if (match === null)
334
+ continue;
335
+ const start = Number(match[1]);
336
+ const end = Number(match[2]);
337
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) {
338
+ if (strict)
339
+ throw new MaterialArchiveRangeError(`invalid segment filename ${file}`);
340
+ continue;
341
+ }
342
+ out.push({ file, start, end });
343
+ }
344
+ return out.sort((left, right) => left.start - right.start || left.end - right.end);
345
+ }
346
+ function detectOverlap(archive, active) {
347
+ const max = Math.min(archive.length, active.length);
348
+ for (let length = max; length > 0; length -= 1) {
349
+ const archiveStart = archive.length - length;
350
+ let matches = true;
351
+ for (let index = 0; index < length; index += 1) {
352
+ if (JSON.stringify(archive[archiveStart + index].record) !== JSON.stringify(active[index].record)) {
353
+ matches = false;
354
+ break;
355
+ }
356
+ }
357
+ if (matches)
358
+ return length;
359
+ }
360
+ return 0;
361
+ }
362
+ function detectOperationalOverlap(path, descriptors, active) {
363
+ const latest = descriptors[descriptors.length - 1];
364
+ if (latest === undefined || active.length === 0)
365
+ return 0;
366
+ const archived = readMaterialsLenient(join(materialArchiveDir(path), latest.file));
367
+ const max = Math.min(archived.length, active.length);
368
+ for (let length = max; length > 0; length -= 1) {
369
+ const archiveStart = archived.length - length;
370
+ let matches = true;
371
+ for (let index = 0; index < length; index += 1) {
372
+ if (JSON.stringify(archived[archiveStart + index]) !== JSON.stringify(active[index])) {
373
+ matches = false;
374
+ break;
375
+ }
376
+ }
377
+ if (matches)
378
+ return length;
379
+ }
380
+ return 0;
381
+ }
382
+ function readCursorsStrict(paths, historyRecords) {
383
+ const out = [];
384
+ for (const path of paths) {
385
+ if (!existsSync(path))
386
+ continue;
387
+ let parsed;
388
+ try {
389
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
390
+ }
391
+ catch {
392
+ throw new InvalidMaterialArchiveCursorError('cursor file is not valid JSON');
393
+ }
394
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
395
+ throw new InvalidMaterialArchiveCursorError('cursor file must contain an object');
396
+ }
397
+ for (const [group, value] of Object.entries(parsed)) {
398
+ if (!Number.isSafeInteger(value) || value < 0 || value > historyRecords) {
399
+ throw new InvalidMaterialArchiveCursorError(`group ${group} is outside history bounds`);
400
+ }
401
+ out.push({ group, position: value });
402
+ }
403
+ }
404
+ return out;
405
+ }
406
+ function minimumCursor(cursors, archiveRecords) {
407
+ return cursors.length === 0 ? archiveRecords : Math.min(...cursors.map((cursor) => cursor.position));
408
+ }
409
+ function archiveableCount(state, minCursor, keepRecords) {
410
+ const committedActive = Math.max(0, minCursor - state.archive.length);
411
+ const tailBound = Math.max(0, state.logicalActive.length - keepRecords);
412
+ return Math.min(committedActive, tailBound);
413
+ }
414
+ function serializeLines(lines) {
415
+ return lines.length === 0 ? '' : `${lines.map((line) => line.raw).join('\n')}\n`;
416
+ }
417
+ function durableReplace(tmpPath, finalPath, contents) {
418
+ let fd = null;
419
+ try {
420
+ fd = openSync(tmpPath, 'w', 0o600);
421
+ writeSync(fd, contents, undefined, 'utf8');
422
+ fsyncSync(fd);
423
+ closeSync(fd);
424
+ fd = null;
425
+ renameSync(tmpPath, finalPath);
426
+ }
427
+ catch (error) {
428
+ if (fd !== null)
429
+ closeSync(fd);
430
+ rmSync(tmpPath, { force: true });
431
+ throw error;
432
+ }
433
+ }
434
+ function fsyncDirectoryBestEffort(path) {
435
+ let fd = null;
436
+ try {
437
+ fd = openSync(path, 'r');
438
+ fsyncSync(fd);
439
+ }
440
+ catch {
441
+ // Windows and some filesystems do not support fsync on directories.
442
+ }
443
+ finally {
444
+ if (fd !== null)
445
+ closeSync(fd);
446
+ }
447
+ }
448
+ function normalizeKeepRecords(value) {
449
+ if (value === undefined)
450
+ return MATERIAL_ARCHIVE_DEFAULT_KEEP_RECORDS;
451
+ if (!Number.isFinite(value) || value < 1)
452
+ throw new RangeError('keepRecords must be a positive integer');
453
+ return Math.floor(value);
454
+ }
455
+ function normalizeRangeStart(value) {
456
+ if (!Number.isFinite(value) || value < 0)
457
+ return 0;
458
+ return Math.floor(value);
459
+ }
460
+ function normalizeRangeCount(value) {
461
+ if (!Number.isFinite(value))
462
+ return Number.MAX_SAFE_INTEGER;
463
+ if (value <= 0)
464
+ return 0;
465
+ return Math.floor(value);
466
+ }
@@ -17,6 +17,7 @@ export declare class MaterialStore {
17
17
  private putLocked;
18
18
  get(materialId: string): Material | undefined;
19
19
  readAll(): Material[];
20
+ readRange(start: number, count: number): Material[];
20
21
  iterate(opts?: {
21
22
  consumerGroup?: string;
22
23
  since?: string;
@@ -1,8 +1,9 @@
1
- import { openSync, writeSync, fsyncSync, closeSync, existsSync, readFileSync, mkdirSync } from 'node:fs';
1
+ import { openSync, writeSync, fsyncSync, closeSync, mkdirSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
3
  import { material } from '../schema/material.js';
4
4
  import { acquireLock, releaseLock } from '../util/fileLock.js';
5
5
  import { assertMaterialSource } from './boundary.js';
6
+ import { readMaterialHistory, readMaterialRange } from './materialArchive.js';
6
7
  /** 原材料库 (append-only jsonl + single-writer + materialId 去重). */
7
8
  export class MaterialStore {
8
9
  path;
@@ -51,18 +52,10 @@ export class MaterialStore {
51
52
  return undefined;
52
53
  }
53
54
  readAll() {
54
- if (!existsSync(this.path))
55
- return [];
56
- const out = [];
57
- for (const l of readFileSync(this.path, 'utf8').split('\n')) {
58
- if (!l)
59
- continue;
60
- try {
61
- out.push(material.parse(JSON.parse(l)));
62
- }
63
- catch { /* skip */ }
64
- }
65
- return out;
55
+ return readMaterialHistory(this.path);
56
+ }
57
+ readRange(start, count) {
58
+ return readMaterialRange(this.path, start, count);
66
59
  }
67
60
  *iterate(opts = {}) {
68
61
  for (const m of this.readAll()) {
@@ -6,8 +6,7 @@
6
6
  // gate (it reads the vendored files at runtime, so this module stays free of cross-rootDir JSON
7
7
  // imports), and savings-core's daily drift-check locks the vendored copies to upstream. To change a
8
8
  // coefficient: change savings-core first, regenerate vectors there, bump spec_version, then
9
- // re-vendor here AND in evomap-hub / evomap-private / evox-online-deck / evomap-desktop / evox /
10
- // evolver-private-dev / evolver-v2-enterprise-dev.
9
+ // re-vendor it into each consuming distribution.
11
10
  //
12
11
  // Core purity contract holds: no I/O, no Date.now/Math.random — pure functions over numbers.
13
12
  export const SAVINGS_SPEC_VERSION = '0.3.0';
@@ -103,4 +103,13 @@ export interface DownloadedArtifact {
103
103
  *
104
104
  * Fail-closed everywhere: any doubt (bad structure, hash gap, signature failure, thrown crypto error) → reject.
105
105
  */
106
- export declare function verifyManifest(manifest: unknown, downloaded: readonly DownloadedArtifact[], publicKey?: string | KeyObject): VerifyResult;
106
+ export declare function verifyManifest(manifest: unknown, downloaded: readonly DownloadedArtifact[], publicKey?: string | KeyObject): VerifyResult;
107
+ /**
108
+ * Verify exactly one explicitly selected download against a complete release manifest.
109
+ *
110
+ * Unlike verifyManifest, this verifier does not require downloading every platform artifact. When a public key is
111
+ * configured it first verifies the Ed25519 signature over the complete canonical manifest, then requires exactly
112
+ * one downloaded artifact and exactly one manifest entry with the same path, and finally checks that artifact's
113
+ * sha256. The complete manifest is never filtered or reconstructed before signature verification.
114
+ */
115
+ export declare function verifySelectedManifestArtifact(manifest: unknown, downloaded: readonly DownloadedArtifact[], publicKey?: string | KeyObject): VerifyResult;
@@ -238,6 +238,25 @@ function toPublicKey(publicKey) {
238
238
  const der = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), raw]);
239
239
  return createPublicKey({ key: der, format: 'der', type: 'spki' });
240
240
  }
241
+ /** Verify the signature over the complete canonical manifest when a public key is configured. */
242
+ function verifyConfiguredManifestSignature(manifest, publicKey) {
243
+ if (!publicKey)
244
+ return { ok: true, reason: 'signature_not_required' };
245
+ if (!manifest.signature)
246
+ return { ok: false, reason: 'signature_required_but_missing' };
247
+ if (manifest.signatureAlg && manifest.signatureAlg !== 'ed25519') {
248
+ return { ok: false, reason: 'signature_alg_unsupported' };
249
+ }
250
+ try {
251
+ const key = toPublicKey(publicKey);
252
+ const ok = cryptoVerify(null, // Ed25519 ignores the digest-name arg
253
+ canonicalManifestBytes(manifest), key, Buffer.from(manifest.signature, 'base64'));
254
+ return ok ? { ok: true, reason: 'signature_verified' } : { ok: false, reason: 'signature_invalid' };
255
+ }
256
+ catch (err) {
257
+ return { ok: false, reason: `signature_verify_error: ${err instanceof Error ? err.message : String(err)}` };
258
+ }
259
+ }
241
260
  /**
242
261
  * Verify downloaded artifacts against the manifest. PURE + deterministic (takes already-read bytes/hashes; does
243
262
  * NO I/O). Two layers:
@@ -274,21 +293,51 @@ export function verifyManifest(manifest, downloaded, publicKey) {
274
293
  return { ok: false, reason: `sha256_mismatch: ${art.path}` };
275
294
  }
276
295
  // Layer 2: Ed25519 signature — enforced ONLY when a key is configured (then fail-closed).
277
- if (publicKey) {
278
- if (!m.signature)
279
- return { ok: false, reason: 'signature_required_but_missing' };
280
- if (m.signatureAlg && m.signatureAlg !== 'ed25519')
281
- return { ok: false, reason: 'signature_alg_unsupported' };
282
- try {
283
- const key = toPublicKey(publicKey);
284
- const ok = cryptoVerify(null, // Ed25519 ignores the digest-name arg
285
- canonicalManifestBytes(m), key, Buffer.from(m.signature, 'base64'));
286
- if (!ok)
287
- return { ok: false, reason: 'signature_invalid' };
288
- }
289
- catch (err) {
290
- return { ok: false, reason: `signature_verify_error: ${err instanceof Error ? err.message : String(err)}` };
291
- }
296
+ const signature = verifyConfiguredManifestSignature(m, publicKey);
297
+ if (!signature.ok)
298
+ return signature;
299
+ return { ok: true, reason: 'verified' };
300
+ }
301
+ /**
302
+ * Verify exactly one explicitly selected download against a complete release manifest.
303
+ *
304
+ * Unlike verifyManifest, this verifier does not require downloading every platform artifact. When a public key is
305
+ * configured it first verifies the Ed25519 signature over the complete canonical manifest, then requires exactly
306
+ * one downloaded artifact and exactly one manifest entry with the same path, and finally checks that artifact's
307
+ * sha256. The complete manifest is never filtered or reconstructed before signature verification.
308
+ */
309
+ export function verifySelectedManifestArtifact(manifest, downloaded, publicKey) {
310
+ const structErr = manifestStructuralError(manifest);
311
+ if (structErr)
312
+ return { ok: false, reason: structErr };
313
+ const m = manifest;
314
+ const signature = verifyConfiguredManifestSignature(m, publicKey);
315
+ if (!signature.ok)
316
+ return signature;
317
+ if (downloaded.length !== 1) {
318
+ return { ok: false, reason: `downloaded_artifact_count_invalid: ${downloaded.length}` };
319
+ }
320
+ const selected = downloaded[0];
321
+ if (!selected || typeof selected.path !== 'string' || !selected.path) {
322
+ return { ok: false, reason: 'downloaded_artifact_path_invalid' };
323
+ }
324
+ const matches = m.artifacts.filter((artifact) => artifact.path === selected.path);
325
+ if (matches.length === 0)
326
+ return { ok: false, reason: `artifact_missing: ${selected.path}` };
327
+ if (matches.length !== 1)
328
+ return { ok: false, reason: `artifact_duplicate: ${selected.path}` };
329
+ const expected = matches[0];
330
+ if (!expected)
331
+ return { ok: false, reason: `artifact_missing: ${selected.path}` };
332
+ let digest;
333
+ if (selected.sha256)
334
+ digest = selected.sha256.toLowerCase();
335
+ else if (selected.bytes)
336
+ digest = sha256Hex(selected.bytes);
337
+ else
338
+ return { ok: false, reason: `artifact_no_content: ${selected.path}` };
339
+ if (digest !== expected.sha256.toLowerCase()) {
340
+ return { ok: false, reason: `sha256_mismatch: ${selected.path}` };
292
341
  }
293
342
  return { ok: true, reason: 'verified' };
294
343
  }
@@ -22,7 +22,7 @@ export declare const artifactRef: z.ZodObject<{
22
22
  }>;
23
23
  export type ArtifactRef = z.infer<typeof artifactRef>;
24
24
  /** runtime 来源 (D11). 'generic-chat' = 任何输出标准 OpenAI/Anthropic messages 的 AI(经 genericChatAdapter 接入). */
25
- export declare const sourceAgent: z.ZodEnum<["claude-code", "codex", "cursor", "gemini", "kimi", "kiro", "opencode", "generic-chat"]>;
25
+ export declare const sourceAgent: z.ZodEnum<["claude-code", "codex", "cursor", "gemini", "antigravity", "kimi", "kiro", "opencode", "generic-chat"]>;
26
26
  export type SourceAgent = z.infer<typeof sourceAgent>;
27
27
  export declare const blastRadius: z.ZodEnum<["file", "module", "package", "system"]>;
28
28
  export type BlastRadius = z.infer<typeof blastRadius>;
@@ -13,5 +13,5 @@ export const artifactRef = z.object({
13
13
  size: z.number().int().nonnegative(),
14
14
  });
15
15
  /** runtime 来源 (D11). 'generic-chat' = 任何输出标准 OpenAI/Anthropic messages 的 AI(经 genericChatAdapter 接入). */
16
- export const sourceAgent = z.enum(['claude-code', 'codex', 'cursor', 'gemini', 'kimi', 'kiro', 'opencode', 'generic-chat']);
16
+ export const sourceAgent = z.enum(['claude-code', 'codex', 'cursor', 'gemini', 'antigravity', 'kimi', 'kiro', 'opencode', 'generic-chat']);
17
17
  export const blastRadius = z.enum(['file', 'module', 'package', 'system']);