@evomap/evolver-core 2.0.0-beta.2 → 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 (58) hide show
  1. package/dist/algo/candidateAssembly.js +9 -6
  2. package/dist/algo/cycleEngine.d.ts +11 -0
  3. package/dist/algo/cycleEngine.js +12 -6
  4. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  5. package/dist/algo/geneSelection.d.ts +12 -1
  6. package/dist/algo/geneSelection.js +24 -8
  7. package/dist/algo/index.d.ts +1 -0
  8. package/dist/algo/index.js +1 -0
  9. package/dist/algo/memoryGraph.d.ts +62 -0
  10. package/dist/algo/memoryGraph.js +86 -0
  11. package/dist/algo/orchestrator.d.ts +3 -0
  12. package/dist/algo/orchestrator.js +14 -2
  13. package/dist/assetstore/assetSidecarRecords.d.ts +23 -0
  14. package/dist/assetstore/assetSidecarRecords.js +142 -0
  15. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  16. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  17. package/dist/assetstore/assetStoreHealth.d.ts +75 -0
  18. package/dist/assetstore/assetStoreHealth.js +277 -0
  19. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  20. package/dist/assetstore/assetStoreLayout.js +6 -0
  21. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  22. package/dist/assetstore/assetStoreStorage.js +318 -0
  23. package/dist/assetstore/assetSyncLedger.d.ts +5 -1
  24. package/dist/assetstore/assetSyncLedger.js +44 -64
  25. package/dist/assetstore/index.d.ts +2 -0
  26. package/dist/assetstore/index.js +2 -0
  27. package/dist/assetstore/localJsonl.d.ts +1 -0
  28. package/dist/assetstore/localJsonl.js +36 -32
  29. package/dist/assetstore/provenance.d.ts +13 -0
  30. package/dist/assetstore/provenance.js +60 -84
  31. package/dist/assetstore/provider.d.ts +2 -0
  32. package/dist/assetstore/reviewFilter.js +3 -1
  33. package/dist/assetstore/reviewLedger.d.ts +8 -2
  34. package/dist/assetstore/reviewLedger.js +71 -45
  35. package/dist/benchmark/index.d.ts +2 -1
  36. package/dist/benchmark/index.js +2 -1
  37. package/dist/benchmark/triggerShift.d.ts +62 -0
  38. package/dist/benchmark/triggerShift.js +106 -0
  39. package/dist/events/ingest.d.ts +1 -1
  40. package/dist/events/ingest.js +2 -0
  41. package/dist/events/paths.d.ts +1 -1
  42. package/dist/events/paths.js +2 -2
  43. package/dist/exec/autoExec.d.ts +6 -1
  44. package/dist/exec/autoExec.js +31 -0
  45. package/dist/exec/autonomousCycle.d.ts +2 -0
  46. package/dist/exec/autonomousCycle.js +5 -0
  47. package/dist/exec/claudeBridge.d.ts +7 -2
  48. package/dist/exec/claudeBridge.js +92 -14
  49. package/dist/exec/prompt.js +9 -0
  50. package/dist/exec/runnerRegistry.d.ts +56 -12
  51. package/dist/exec/runnerRegistry.js +272 -22
  52. package/dist/hub/bindings.js +12 -2
  53. package/dist/ops/savingsCore.js +1 -2
  54. package/dist/ops/selfUpdate.d.ts +10 -1
  55. package/dist/ops/selfUpdate.js +64 -15
  56. package/dist/util/fileLock.d.ts +19 -2
  57. package/dist/util/fileLock.js +166 -31
  58. package/package.json +5 -1
@@ -0,0 +1,277 @@
1
+ import { lstatSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { acquireLock, releaseLock } from '../util/fileLock.js';
4
+ import { validateWire, verifyAssetId } from '../wire/index.js';
5
+ import { LOCAL_ASSET_FILES } from './assetStoreLayout.js';
6
+ import { assertOptionalRegularFile, isReliableAssetStoreLockRelease, readUtf8Regular, UnsafeAssetStorePathError, } from './assetStoreStorage.js';
7
+ import { parseAssetSyncRecord, parseProvenanceRecord, parseReviewRecord, parseSidecarJsonl, } from './assetSidecarRecords.js';
8
+ export const DEFAULT_ASSET_HEALTH_MAX_FILE_BYTES = 64 * 1024 * 1024;
9
+ const LOCAL_ASSET_SIDECARS = [
10
+ { kind: 'provenance', file: 'provenance.jsonl', parseRecord: parseProvenanceRecord },
11
+ { kind: 'review', file: 'review.jsonl', parseRecord: parseReviewRecord },
12
+ { kind: 'asset-sync', file: 'asset-sync.jsonl', parseRecord: parseAssetSyncRecord },
13
+ ];
14
+ function healthScanLimit(value) {
15
+ if (value === undefined || !Number.isFinite(value))
16
+ return DEFAULT_ASSET_HEALTH_MAX_FILE_BYTES;
17
+ return Math.min(DEFAULT_ASSET_HEALTH_MAX_FILE_BYTES, Math.max(1, Math.floor(value)));
18
+ }
19
+ function isErrno(error, code) {
20
+ return typeof error === 'object' && error !== null && error.code === code;
21
+ }
22
+ function emptyFile(kind, file, status, reason) {
23
+ return {
24
+ kind,
25
+ file,
26
+ status,
27
+ bytes: 0,
28
+ rows: 0,
29
+ validRows: 0,
30
+ uniqueAssets: 0,
31
+ duplicateRows: 0,
32
+ corruptRows: 0,
33
+ hashMismatchRows: 0,
34
+ schemaInvalidRows: 0,
35
+ unterminated: false,
36
+ ...(reason ? { reason } : {}),
37
+ };
38
+ }
39
+ function allFiles(status, reason) {
40
+ return Object.entries(LOCAL_ASSET_FILES)
41
+ .map(([kind, file]) => emptyFile(kind, file, status, reason));
42
+ }
43
+ function emptySidecar(kind, file, status, reason) {
44
+ return {
45
+ kind,
46
+ file,
47
+ status,
48
+ bytes: 0,
49
+ rows: 0,
50
+ validRows: 0,
51
+ corruptRows: 0,
52
+ unterminated: false,
53
+ ...(reason ? { reason } : {}),
54
+ };
55
+ }
56
+ function allSidecars(status, reason) {
57
+ return LOCAL_ASSET_SIDECARS.map(({ kind, file }) => emptySidecar(kind, file, status, reason));
58
+ }
59
+ function summarize(files, sidecars) {
60
+ const totals = {
61
+ files: files.length,
62
+ missingFiles: files.filter((file) => file.status === 'missing').length,
63
+ unsafeFiles: files.filter((file) => file.status === 'unsafe').length,
64
+ unavailableFiles: files.filter((file) => file.status === 'unavailable').length,
65
+ bytes: files.reduce((sum, file) => sum + file.bytes, 0),
66
+ rows: files.reduce((sum, file) => sum + file.rows, 0),
67
+ validRows: files.reduce((sum, file) => sum + file.validRows, 0),
68
+ uniqueAssets: files.reduce((sum, file) => sum + file.uniqueAssets, 0),
69
+ duplicateRows: files.reduce((sum, file) => sum + file.duplicateRows, 0),
70
+ corruptRows: files.reduce((sum, file) => sum + file.corruptRows, 0),
71
+ hashMismatchRows: files.reduce((sum, file) => sum + file.hashMismatchRows, 0),
72
+ schemaInvalidRows: files.reduce((sum, file) => sum + file.schemaInvalidRows, 0),
73
+ unterminatedFiles: files.filter((file) => file.unterminated).length,
74
+ };
75
+ const sidecarTotals = {
76
+ files: sidecars.length,
77
+ missingFiles: sidecars.filter((file) => file.status === 'missing').length,
78
+ unsafeFiles: sidecars.filter((file) => file.status === 'unsafe').length,
79
+ unavailableFiles: sidecars.filter((file) => file.status === 'unavailable').length,
80
+ bytes: sidecars.reduce((sum, file) => sum + file.bytes, 0),
81
+ rows: sidecars.reduce((sum, file) => sum + file.rows, 0),
82
+ validRows: sidecars.reduce((sum, file) => sum + file.validRows, 0),
83
+ corruptRows: sidecars.reduce((sum, file) => sum + file.corruptRows, 0),
84
+ unterminatedFiles: sidecars.filter((file) => file.unterminated).length,
85
+ };
86
+ const status = totals.unsafeFiles > 0 || sidecarTotals.unsafeFiles > 0
87
+ ? 'unsafe'
88
+ : totals.unavailableFiles > 0 || sidecarTotals.unavailableFiles > 0
89
+ ? 'unavailable'
90
+ : totals.duplicateRows > 0
91
+ || totals.corruptRows > 0
92
+ || totals.hashMismatchRows > 0
93
+ || totals.schemaInvalidRows > 0
94
+ || totals.unterminatedFiles > 0
95
+ || sidecarTotals.corruptRows > 0
96
+ || sidecarTotals.unterminatedFiles > 0
97
+ ? 'degraded'
98
+ : 'healthy';
99
+ return {
100
+ ok: status === 'healthy',
101
+ status,
102
+ totals,
103
+ files: [...files],
104
+ sidecarTotals,
105
+ sidecars: [...sidecars],
106
+ };
107
+ }
108
+ function inspectFile(baseDir, kind, file, maxFileBytes) {
109
+ try {
110
+ const path = join(baseDir, file);
111
+ const stat = assertOptionalRegularFile(path);
112
+ if (stat === null)
113
+ return emptyFile(kind, file, 'missing');
114
+ if (stat.size > maxFileBytes) {
115
+ return { ...emptyFile(kind, file, 'unavailable', 'scan_limit_exceeded'), bytes: stat.size };
116
+ }
117
+ const raw = readUtf8Regular(path);
118
+ if (raw === null)
119
+ return emptyFile(kind, file, 'unavailable', 'read_unavailable');
120
+ const rows = raw.split('\n').filter((line) => line.trim().length > 0);
121
+ const seen = new Set();
122
+ let validRows = 0;
123
+ let duplicateRows = 0;
124
+ let corruptRows = 0;
125
+ let hashMismatchRows = 0;
126
+ let schemaInvalidRows = 0;
127
+ for (const line of rows) {
128
+ try {
129
+ const value = JSON.parse(line);
130
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
131
+ corruptRows += 1;
132
+ continue;
133
+ }
134
+ const record = value;
135
+ const assetId = typeof record['asset_id'] === 'string' ? record['asset_id'].trim() : '';
136
+ if (!assetId || record['type'] !== kind) {
137
+ corruptRows += 1;
138
+ continue;
139
+ }
140
+ if (seen.has(assetId))
141
+ duplicateRows += 1;
142
+ else
143
+ seen.add(assetId);
144
+ if (!verifyAssetId(record)) {
145
+ hashMismatchRows += 1;
146
+ continue;
147
+ }
148
+ if (kind !== 'AntiGene' && !validateWire(record).ok) {
149
+ schemaInvalidRows += 1;
150
+ continue;
151
+ }
152
+ validRows += 1;
153
+ }
154
+ catch {
155
+ corruptRows += 1;
156
+ }
157
+ }
158
+ const unterminated = raw.length > 0 && !raw.endsWith('\n');
159
+ const status = duplicateRows > 0
160
+ || corruptRows > 0
161
+ || hashMismatchRows > 0
162
+ || schemaInvalidRows > 0
163
+ || unterminated
164
+ ? 'degraded'
165
+ : 'ok';
166
+ return {
167
+ kind,
168
+ file,
169
+ status,
170
+ bytes: Buffer.byteLength(raw, 'utf8'),
171
+ rows: rows.length,
172
+ validRows,
173
+ uniqueAssets: seen.size,
174
+ duplicateRows,
175
+ corruptRows,
176
+ hashMismatchRows,
177
+ schemaInvalidRows,
178
+ unterminated,
179
+ };
180
+ }
181
+ catch (error) {
182
+ if (error instanceof UnsafeAssetStorePathError)
183
+ return emptyFile(kind, file, 'unsafe', error.reason);
184
+ return emptyFile(kind, file, 'unavailable', 'read_unavailable');
185
+ }
186
+ }
187
+ function inspectSidecar(baseDir, definition, maxFileBytes) {
188
+ const { kind, file, parseRecord } = definition;
189
+ try {
190
+ const path = join(baseDir, file);
191
+ const stat = assertOptionalRegularFile(path);
192
+ if (stat === null)
193
+ return emptySidecar(kind, file, 'missing');
194
+ if (stat.size > maxFileBytes) {
195
+ return { ...emptySidecar(kind, file, 'unavailable', 'scan_limit_exceeded'), bytes: stat.size };
196
+ }
197
+ const raw = readUtf8Regular(path);
198
+ if (raw === null)
199
+ return emptySidecar(kind, file, 'unavailable', 'read_unavailable');
200
+ const parsed = parseSidecarJsonl(raw, parseRecord);
201
+ const status = parsed.corruptRows > 0 || parsed.unterminated ? 'degraded' : 'ok';
202
+ return {
203
+ kind,
204
+ file,
205
+ status,
206
+ bytes: Buffer.byteLength(raw, 'utf8'),
207
+ rows: parsed.rows,
208
+ validRows: parsed.validRows,
209
+ corruptRows: parsed.corruptRows,
210
+ unterminated: parsed.unterminated,
211
+ };
212
+ }
213
+ catch (error) {
214
+ if (error instanceof UnsafeAssetStorePathError)
215
+ return emptySidecar(kind, file, 'unsafe', error.reason);
216
+ return emptySidecar(kind, file, 'unavailable', 'read_unavailable');
217
+ }
218
+ }
219
+ function directoryState(baseDir) {
220
+ try {
221
+ const stat = lstatSync(baseDir);
222
+ if (stat.isSymbolicLink() || !stat.isDirectory())
223
+ return 'unsafe';
224
+ return 'safe';
225
+ }
226
+ catch (error) {
227
+ if (isErrno(error, 'ENOENT'))
228
+ return 'missing';
229
+ return 'unavailable';
230
+ }
231
+ }
232
+ export function inspectLocalAssetStore(baseDir, opts = {}, deps = {}) {
233
+ const state = directoryState(baseDir);
234
+ if (state === 'missing')
235
+ return summarize(allFiles('missing'), allSidecars('missing'));
236
+ if (state === 'unsafe') {
237
+ return summarize(allFiles('unsafe', 'base_directory'), allSidecars('unsafe', 'base_directory'));
238
+ }
239
+ if (state === 'unavailable') {
240
+ return summarize(allFiles('unavailable', 'read_unavailable'), allSidecars('unavailable', 'read_unavailable'));
241
+ }
242
+ const lockPath = join(baseDir, '.assetstore.lock');
243
+ try {
244
+ assertOptionalRegularFile(lockPath, 'lock_file');
245
+ }
246
+ catch (error) {
247
+ if (error instanceof UnsafeAssetStorePathError) {
248
+ return summarize(allFiles('unsafe', error.reason), allSidecars('unsafe', error.reason));
249
+ }
250
+ return summarize(allFiles('unavailable', 'lock_unavailable'), allSidecars('unavailable', 'lock_unavailable'));
251
+ }
252
+ try {
253
+ (deps.acquireLock ?? acquireLock)(lockPath);
254
+ }
255
+ catch {
256
+ return summarize(allFiles('unavailable', 'lock_unavailable'), allSidecars('unavailable', 'lock_unavailable'));
257
+ }
258
+ const maxFileBytes = healthScanLimit(opts.maxFileBytes);
259
+ let report;
260
+ try {
261
+ report = summarize(Object.entries(LOCAL_ASSET_FILES)
262
+ .map(([kind, file]) => inspectFile(baseDir, kind, file, maxFileBytes)), LOCAL_ASSET_SIDECARS.map((definition) => inspectSidecar(baseDir, definition, maxFileBytes)));
263
+ }
264
+ catch {
265
+ report = summarize(allFiles('unavailable', 'read_unavailable'), allSidecars('unavailable', 'read_unavailable'));
266
+ }
267
+ try {
268
+ const released = (deps.releaseLock ?? releaseLock)(lockPath);
269
+ if (!isReliableAssetStoreLockRelease(released)) {
270
+ return summarize(allFiles('unavailable', 'lock_unavailable'), allSidecars('unavailable', 'lock_unavailable'));
271
+ }
272
+ }
273
+ catch {
274
+ return summarize(allFiles('unavailable', 'lock_unavailable'), allSidecars('unavailable', 'lock_unavailable'));
275
+ }
276
+ return report;
277
+ }
@@ -0,0 +1,2 @@
1
+ import type { AssetKind } from './provider.js';
2
+ export declare const LOCAL_ASSET_FILES: Readonly<Record<AssetKind, string>>;
@@ -0,0 +1,6 @@
1
+ export const LOCAL_ASSET_FILES = {
2
+ Gene: 'genes.jsonl',
3
+ Capsule: 'capsules.jsonl',
4
+ EvolutionEvent: 'events.jsonl',
5
+ AntiGene: 'anti-genes.jsonl',
6
+ };
@@ -0,0 +1,42 @@
1
+ import type { Stats } from 'node:fs';
2
+ import { acquireLock, releaseLock, type ReleaseLockResult } from '../util/fileLock.js';
3
+ export type UnsafeAssetStorePathReason = 'symlink' | 'not_directory' | 'not_regular_file' | 'path_changed';
4
+ export type AssetStorePathRole = 'base_directory' | 'asset_file' | 'lock_file' | 'temp_file';
5
+ export declare class UnsafeAssetStorePathError extends Error {
6
+ readonly role: AssetStorePathRole;
7
+ readonly reason: UnsafeAssetStorePathReason;
8
+ readonly code = "UNSAFE_ASSET_STORE_PATH";
9
+ constructor(role: AssetStorePathRole, reason: UnsafeAssetStorePathReason);
10
+ }
11
+ export declare class AssetStoreReadLimitError extends Error {
12
+ readonly code = "ASSET_STORE_READ_LIMIT";
13
+ constructor();
14
+ }
15
+ export interface DurableWriteOptions {
16
+ syncFile?: (fd: number) => void;
17
+ syncDirectory?: (path: string) => void;
18
+ onTempPath?: (path: string) => void;
19
+ }
20
+ export interface AssetStoreLockDeps {
21
+ acquireLock?: typeof acquireLock;
22
+ releaseLock?: typeof releaseLock;
23
+ }
24
+ export declare function ensureAssetStoreDirectory(path: string): void;
25
+ /** Validate an existing store root without recreating a deleted or unmounted path. */
26
+ export declare function assertAssetStoreDirectory(path: string): void;
27
+ export declare function isReliableAssetStoreLockRelease(result: ReleaseLockResult): boolean;
28
+ /**
29
+ * Run one synchronous asset-store critical section without hiding its primary failure.
30
+ * A release failure is surfaced only after a successful operation; when both fail, the operation error wins.
31
+ */
32
+ export declare function withAssetStoreLock<T>(lockPath: string, operation: () => T, deps?: AssetStoreLockDeps): T;
33
+ export declare function assertOptionalRegularFile(path: string, role?: AssetStorePathRole): Stats | null;
34
+ export declare function regularFileFingerprint(path: string): string;
35
+ export declare function readRegularBuffer(path: string, maxBytes?: number): Buffer | null;
36
+ export declare function readUtf8Regular(path: string): string | null;
37
+ export declare function createBufferDurableExclusive(path: string, value: Buffer, opts?: DurableWriteOptions): void;
38
+ export declare function fsyncDirectoryBestEffort(path: string): void;
39
+ export declare function appendUtf8Durable(path: string, value: string, opts?: DurableWriteOptions): void;
40
+ export declare function replaceUtf8Durable(path: string, value: string, opts?: DurableWriteOptions): void;
41
+ /** Remove one exact UTF-8 suffix and fsync the new file length; false means the suffix was not current. */
42
+ export declare function truncateUtf8SuffixDurable(path: string, suffix: string, opts?: Pick<DurableWriteOptions, 'syncFile'>): boolean;
@@ -0,0 +1,318 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, unlinkSync, writeSync, } from 'node:fs';
3
+ import { basename, dirname, join } from 'node:path';
4
+ import { acquireLock, LockReleaseError, releaseLock, } from '../util/fileLock.js';
5
+ export class UnsafeAssetStorePathError extends Error {
6
+ role;
7
+ reason;
8
+ code = 'UNSAFE_ASSET_STORE_PATH';
9
+ constructor(role, reason) {
10
+ super(`unsafe asset store ${role}: ${reason}`);
11
+ this.role = role;
12
+ this.reason = reason;
13
+ this.name = 'UnsafeAssetStorePathError';
14
+ }
15
+ }
16
+ export class AssetStoreReadLimitError extends Error {
17
+ code = 'ASSET_STORE_READ_LIMIT';
18
+ constructor() {
19
+ super('asset store file exceeds the configured read limit');
20
+ this.name = 'AssetStoreReadLimitError';
21
+ }
22
+ }
23
+ function isErrno(error, code) {
24
+ return typeof error === 'object' && error !== null && error.code === code;
25
+ }
26
+ function noFollowFlag() {
27
+ return constants['O_NOFOLLOW'] ?? 0;
28
+ }
29
+ function statOrNull(path) {
30
+ try {
31
+ return lstatSync(path);
32
+ }
33
+ catch (error) {
34
+ if (isErrno(error, 'ENOENT'))
35
+ return null;
36
+ throw error;
37
+ }
38
+ }
39
+ function assertRegularStat(stat, role) {
40
+ if (stat.isSymbolicLink())
41
+ throw new UnsafeAssetStorePathError(role, 'symlink');
42
+ if (!stat.isFile())
43
+ throw new UnsafeAssetStorePathError(role, 'not_regular_file');
44
+ return stat;
45
+ }
46
+ export function ensureAssetStoreDirectory(path) {
47
+ let stat = statOrNull(path);
48
+ if (stat === null) {
49
+ mkdirSync(path, { recursive: true, mode: 0o700 });
50
+ stat = statOrNull(path);
51
+ }
52
+ assertAssetStoreDirectoryStat(stat);
53
+ }
54
+ function assertAssetStoreDirectoryStat(stat) {
55
+ if (stat === null)
56
+ throw new UnsafeAssetStorePathError('base_directory', 'not_directory');
57
+ if (stat.isSymbolicLink())
58
+ throw new UnsafeAssetStorePathError('base_directory', 'symlink');
59
+ if (!stat.isDirectory())
60
+ throw new UnsafeAssetStorePathError('base_directory', 'not_directory');
61
+ }
62
+ /** Validate an existing store root without recreating a deleted or unmounted path. */
63
+ export function assertAssetStoreDirectory(path) {
64
+ assertAssetStoreDirectoryStat(statOrNull(path));
65
+ }
66
+ export function isReliableAssetStoreLockRelease(result) {
67
+ return result.released
68
+ && (result.reason === 'released' || result.reason === 'released_with_cleanup_error');
69
+ }
70
+ /**
71
+ * Run one synchronous asset-store critical section without hiding its primary failure.
72
+ * A release failure is surfaced only after a successful operation; when both fail, the operation error wins.
73
+ */
74
+ export function withAssetStoreLock(lockPath, operation, deps = {}) {
75
+ assertOptionalRegularFile(lockPath, 'lock_file');
76
+ (deps.acquireLock ?? acquireLock)(lockPath);
77
+ let value;
78
+ let operationError;
79
+ let operationFailed = false;
80
+ try {
81
+ value = operation();
82
+ }
83
+ catch (error) {
84
+ operationFailed = true;
85
+ operationError = error;
86
+ }
87
+ let releaseError;
88
+ try {
89
+ const released = (deps.releaseLock ?? releaseLock)(lockPath);
90
+ if (!isReliableAssetStoreLockRelease(released))
91
+ releaseError = new LockReleaseError(released.reason);
92
+ }
93
+ catch (error) {
94
+ releaseError = error;
95
+ }
96
+ if (operationFailed)
97
+ throw operationError;
98
+ if (releaseError !== undefined)
99
+ throw releaseError;
100
+ return value;
101
+ }
102
+ export function assertOptionalRegularFile(path, role = 'asset_file') {
103
+ const stat = statOrNull(path);
104
+ return stat === null ? null : assertRegularStat(stat, role);
105
+ }
106
+ export function regularFileFingerprint(path) {
107
+ try {
108
+ const stat = lstatSync(path, { bigint: true });
109
+ if (stat.isSymbolicLink())
110
+ throw new UnsafeAssetStorePathError('asset_file', 'symlink');
111
+ if (!stat.isFile())
112
+ throw new UnsafeAssetStorePathError('asset_file', 'not_regular_file');
113
+ return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
114
+ }
115
+ catch (error) {
116
+ if (isErrno(error, 'ENOENT'))
117
+ return 'missing';
118
+ throw error;
119
+ }
120
+ }
121
+ function assertOpenedPathMatches(fd, path, role) {
122
+ const opened = fstatSync(fd, { bigint: true });
123
+ if (!opened.isFile())
124
+ throw new UnsafeAssetStorePathError(role, 'not_regular_file');
125
+ let current;
126
+ try {
127
+ current = lstatSync(path, { bigint: true });
128
+ }
129
+ catch (error) {
130
+ if (isErrno(error, 'ENOENT'))
131
+ throw new UnsafeAssetStorePathError(role, 'path_changed');
132
+ throw error;
133
+ }
134
+ if (current.isSymbolicLink())
135
+ throw new UnsafeAssetStorePathError(role, 'symlink');
136
+ if (!current.isFile())
137
+ throw new UnsafeAssetStorePathError(role, 'not_regular_file');
138
+ if (opened.dev !== current.dev || opened.ino !== current.ino) {
139
+ throw new UnsafeAssetStorePathError(role, 'path_changed');
140
+ }
141
+ }
142
+ function openNoFollow(path, flags, mode) {
143
+ return mode === undefined
144
+ ? openSync(path, flags | noFollowFlag())
145
+ : openSync(path, flags | noFollowFlag(), mode);
146
+ }
147
+ export function readRegularBuffer(path, maxBytes = Number.MAX_SAFE_INTEGER) {
148
+ if (assertOptionalRegularFile(path) === null)
149
+ return null;
150
+ const fd = openNoFollow(path, constants.O_RDONLY);
151
+ try {
152
+ assertOpenedPathMatches(fd, path, 'asset_file');
153
+ if (fstatSync(fd).size > maxBytes)
154
+ throw new AssetStoreReadLimitError();
155
+ const value = readFileSync(fd);
156
+ if (value.byteLength > maxBytes)
157
+ throw new AssetStoreReadLimitError();
158
+ return value;
159
+ }
160
+ finally {
161
+ closeSync(fd);
162
+ }
163
+ }
164
+ export function readUtf8Regular(path) {
165
+ return readRegularBuffer(path)?.toString('utf8') ?? null;
166
+ }
167
+ function writeAll(fd, value) {
168
+ const bytes = typeof value === 'string' ? Buffer.from(value, 'utf8') : value;
169
+ let offset = 0;
170
+ while (offset < bytes.length) {
171
+ const written = writeSync(fd, bytes, offset, bytes.length - offset);
172
+ if (written <= 0)
173
+ throw new Error('asset store write made no progress');
174
+ offset += written;
175
+ }
176
+ }
177
+ export function createBufferDurableExclusive(path, value, opts = {}) {
178
+ const parent = dirname(path);
179
+ assertAssetStoreDirectory(parent);
180
+ const fd = openNoFollow(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
181
+ let operationError;
182
+ let openedIdentity;
183
+ try {
184
+ assertOpenedPathMatches(fd, path, 'asset_file');
185
+ const opened = fstatSync(fd, { bigint: true });
186
+ openedIdentity = { dev: opened.dev, ino: opened.ino };
187
+ writeAll(fd, value);
188
+ (opts.syncFile ?? fsyncSync)(fd);
189
+ }
190
+ catch (error) {
191
+ operationError = error;
192
+ }
193
+ finally {
194
+ try {
195
+ closeSync(fd);
196
+ }
197
+ catch (error) {
198
+ if (operationError === undefined)
199
+ operationError = error;
200
+ }
201
+ }
202
+ if (operationError !== undefined) {
203
+ try {
204
+ const current = lstatSync(path, { bigint: true });
205
+ if (openedIdentity
206
+ && !current.isSymbolicLink()
207
+ && current.isFile()
208
+ && current.dev === openedIdentity.dev
209
+ && current.ino === openedIdentity.ino) {
210
+ unlinkSync(path);
211
+ }
212
+ }
213
+ catch { /* preserve the primary write/fsync failure */ }
214
+ throw operationError;
215
+ }
216
+ (opts.syncDirectory ?? fsyncDirectoryBestEffort)(parent);
217
+ }
218
+ export function fsyncDirectoryBestEffort(path) {
219
+ let fd;
220
+ try {
221
+ fd = openSync(path, constants.O_RDONLY);
222
+ fsyncSync(fd);
223
+ }
224
+ catch {
225
+ // Windows and some filesystems do not support syncing directory handles.
226
+ }
227
+ finally {
228
+ if (fd !== undefined) {
229
+ try {
230
+ closeSync(fd);
231
+ }
232
+ catch { /* best-effort directory sync must stay best-effort */ }
233
+ }
234
+ }
235
+ }
236
+ export function appendUtf8Durable(path, value, opts = {}) {
237
+ const parent = dirname(path);
238
+ assertAssetStoreDirectory(parent);
239
+ const existed = assertOptionalRegularFile(path) !== null;
240
+ const fd = openNoFollow(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT, 0o600);
241
+ try {
242
+ assertOpenedPathMatches(fd, path, 'asset_file');
243
+ writeAll(fd, value);
244
+ (opts.syncFile ?? fsyncSync)(fd);
245
+ }
246
+ finally {
247
+ closeSync(fd);
248
+ }
249
+ if (!existed)
250
+ (opts.syncDirectory ?? fsyncDirectoryBestEffort)(parent);
251
+ }
252
+ function removeTemp(path) {
253
+ try {
254
+ unlinkSync(path);
255
+ }
256
+ catch (error) {
257
+ if (!isErrno(error, 'ENOENT'))
258
+ throw error;
259
+ }
260
+ }
261
+ export function replaceUtf8Durable(path, value, opts = {}) {
262
+ const parent = dirname(path);
263
+ assertAssetStoreDirectory(parent);
264
+ assertOptionalRegularFile(path);
265
+ const tempPath = join(parent, `.${basename(path)}.compact.${process.pid}.${randomUUID()}.tmp`);
266
+ opts.onTempPath?.(tempPath);
267
+ let renamed = false;
268
+ try {
269
+ const fd = openNoFollow(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
270
+ try {
271
+ assertOpenedPathMatches(fd, tempPath, 'temp_file');
272
+ writeAll(fd, value);
273
+ (opts.syncFile ?? fsyncSync)(fd);
274
+ }
275
+ finally {
276
+ closeSync(fd);
277
+ }
278
+ assertOptionalRegularFile(path);
279
+ renameSync(tempPath, path);
280
+ renamed = true;
281
+ (opts.syncDirectory ?? fsyncDirectoryBestEffort)(parent);
282
+ }
283
+ finally {
284
+ if (!renamed)
285
+ removeTemp(tempPath);
286
+ }
287
+ }
288
+ /** Remove one exact UTF-8 suffix and fsync the new file length; false means the suffix was not current. */
289
+ export function truncateUtf8SuffixDurable(path, suffix, opts = {}) {
290
+ if (assertOptionalRegularFile(path) === null)
291
+ return false;
292
+ const expected = Buffer.from(suffix, 'utf8');
293
+ if (expected.length === 0)
294
+ return false;
295
+ const fd = openNoFollow(path, constants.O_RDWR);
296
+ try {
297
+ assertOpenedPathMatches(fd, path, 'asset_file');
298
+ const stat = fstatSync(fd);
299
+ if (stat.size < expected.length)
300
+ return false;
301
+ const actual = Buffer.alloc(expected.length);
302
+ let offset = 0;
303
+ while (offset < actual.length) {
304
+ const read = readSync(fd, actual, offset, actual.length - offset, stat.size - actual.length + offset);
305
+ if (read === 0)
306
+ return false;
307
+ offset += read;
308
+ }
309
+ if (!actual.equals(expected))
310
+ return false;
311
+ ftruncateSync(fd, stat.size - expected.length);
312
+ (opts.syncFile ?? fsyncSync)(fd);
313
+ return true;
314
+ }
315
+ finally {
316
+ closeSync(fd);
317
+ }
318
+ }
@@ -16,7 +16,9 @@ export interface AssetSyncRecord {
16
16
  export declare class AssetSyncLedger {
17
17
  private readonly now;
18
18
  private readonly path;
19
+ private readonly lockPath;
19
20
  private readonly index;
21
+ private fileState;
20
22
  private loaded;
21
23
  constructor(baseDir: string, now?: () => number);
22
24
  append(rec: Omit<AssetSyncRecord, 'syncedAt'> & {
@@ -24,5 +26,7 @@ export declare class AssetSyncLedger {
24
26
  }): AssetSyncRecord;
25
27
  get(assetId: string): AssetSyncRecord | null;
26
28
  list(): AssetSyncRecord[];
27
- private load;
29
+ private rebuildIndex;
30
+ private refreshUnderLock;
31
+ private withFreshRead;
28
32
  }