@evomap/evolver-core 2.0.0-beta.9 → 2.0.0

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 (142) hide show
  1. package/dist/algo/candidateAssembly.d.ts +7 -2
  2. package/dist/algo/candidateAssembly.js +81 -13
  3. package/dist/algo/capabilityCandidates.js +10 -0
  4. package/dist/algo/cycleEngine.d.ts +23 -2
  5. package/dist/algo/cycleEngine.js +236 -36
  6. package/dist/algo/exploration.d.ts +7 -0
  7. package/dist/algo/exploration.js +16 -3
  8. package/dist/algo/geneHealth.d.ts +2 -2
  9. package/dist/algo/geneHealth.js +5 -4
  10. package/dist/algo/geneSelection.d.ts +63 -5
  11. package/dist/algo/geneSelection.js +330 -30
  12. package/dist/algo/index.d.ts +2 -0
  13. package/dist/algo/index.js +2 -0
  14. package/dist/algo/orchestrator.d.ts +8 -1
  15. package/dist/algo/orchestrator.js +15 -3
  16. package/dist/algo/publishEligibility.d.ts +34 -0
  17. package/dist/algo/publishEligibility.js +52 -0
  18. package/dist/algo/ucb1.d.ts +53 -0
  19. package/dist/algo/ucb1.js +156 -0
  20. package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
  21. package/dist/assetstore/assetSidecarRecords.js +238 -0
  22. package/dist/assetstore/assetSidecarRecovery.js +2 -2
  23. package/dist/assetstore/assetStoreHealth.d.ts +6 -0
  24. package/dist/assetstore/assetStoreHealth.js +56 -14
  25. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  26. package/dist/assetstore/assetStoreStorage.js +27 -9
  27. package/dist/assetstore/assetSyncLedger.d.ts +86 -0
  28. package/dist/assetstore/assetSyncLedger.js +683 -4
  29. package/dist/assetstore/index.d.ts +1 -0
  30. package/dist/assetstore/index.js +1 -0
  31. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  32. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  33. package/dist/assetstore/localJsonl.d.ts +4 -2
  34. package/dist/assetstore/localJsonl.js +89 -9
  35. package/dist/assetstore/provenance.d.ts +80 -4
  36. package/dist/assetstore/provenance.js +313 -5
  37. package/dist/assetstore/provider.d.ts +58 -1
  38. package/dist/assetstore/provider.js +97 -6
  39. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  40. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  41. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  42. package/dist/benchmark/antiGeneRollout.js +4 -3
  43. package/dist/benchmark/index.d.ts +2 -1
  44. package/dist/benchmark/index.js +2 -1
  45. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  46. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  47. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  48. package/dist/bootstrap/envFingerprint.js +5 -0
  49. package/dist/bootstrap/index.d.ts +2 -1
  50. package/dist/bootstrap/index.js +2 -1
  51. package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
  52. package/dist/bootstrap/v1EnvCompat.js +280 -0
  53. package/dist/events/eventArchive.d.ts +2 -0
  54. package/dist/events/eventArchive.js +13 -3
  55. package/dist/events/eventSchema.d.ts +7 -7
  56. package/dist/events/eventStore.d.ts +2 -0
  57. package/dist/events/eventStore.js +5 -1
  58. package/dist/events/ingest.d.ts +1 -0
  59. package/dist/events/ingest.js +1 -0
  60. package/dist/events/paths.d.ts +3 -1
  61. package/dist/events/paths.js +4 -0
  62. package/dist/events/public.d.ts +2 -2
  63. package/dist/events/public.js +2 -2
  64. package/dist/events/reports.d.ts +2 -0
  65. package/dist/events/reports.js +4 -0
  66. package/dist/exec/autoExec.d.ts +43 -2
  67. package/dist/exec/autoExec.js +76 -9
  68. package/dist/exec/autonomousCycle.d.ts +22 -4
  69. package/dist/exec/autonomousCycle.js +64 -13
  70. package/dist/exec/claudeBridge.d.ts +32 -7
  71. package/dist/exec/claudeBridge.js +281 -29
  72. package/dist/exec/prompt.js +5 -1
  73. package/dist/exec/runnerRegistry.d.ts +68 -26
  74. package/dist/exec/runnerRegistry.js +307 -72
  75. package/dist/exec/selfPr.js +1 -7
  76. package/dist/feedback/envelope.d.ts +61 -0
  77. package/dist/feedback/envelope.js +168 -0
  78. package/dist/feedback/index.d.ts +1 -0
  79. package/dist/feedback/index.js +1 -0
  80. package/dist/hooks/hooks.js +1 -0
  81. package/dist/hub/assetCallLog.d.ts +35 -1
  82. package/dist/hub/assetCallLog.js +124 -1
  83. package/dist/hub/bindings.d.ts +8 -1
  84. package/dist/hub/bindings.js +17 -6
  85. package/dist/hub/capability.d.ts +90 -3
  86. package/dist/hub/fake.d.ts +2 -2
  87. package/dist/hub/fake.js +1 -1
  88. package/dist/hub/questionGenerator.d.ts +5 -1
  89. package/dist/hub/questionGenerator.js +8 -6
  90. package/dist/index.d.ts +3 -1
  91. package/dist/index.js +4 -1
  92. package/dist/mailbox/dispatch.d.ts +1 -1
  93. package/dist/mailbox/dispatch.js +22 -6
  94. package/dist/mailbox/envelope.d.ts +7 -1
  95. package/dist/mailbox/envelope.js +9 -2
  96. package/dist/mailbox/ipcServer.d.ts +10 -2
  97. package/dist/mailbox/ipcServer.js +163 -13
  98. package/dist/mailbox/store.d.ts +89 -3
  99. package/dist/mailbox/store.js +895 -41
  100. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  101. package/dist/ops/evolutionGraphProjection.js +315 -0
  102. package/dist/ops/index.d.ts +2 -1
  103. package/dist/ops/index.js +2 -1
  104. package/dist/ops/selfUpdate.d.ts +8 -0
  105. package/dist/ops/selfUpdate.js +24 -8
  106. package/dist/personality/schema.d.ts +16 -16
  107. package/dist/schema/evolutionGraph.d.ts +784 -0
  108. package/dist/schema/evolutionGraph.js +187 -0
  109. package/dist/schema/index.d.ts +1 -0
  110. package/dist/schema/index.js +1 -0
  111. package/dist/schema/signal.d.ts +3 -3
  112. package/dist/schema/signal.js +1 -1
  113. package/dist/signals/curriculum.d.ts +55 -0
  114. package/dist/signals/curriculum.js +202 -0
  115. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  116. package/dist/signals/expand.d.ts +15 -1
  117. package/dist/signals/expand.js +169 -1
  118. package/dist/signals/extractor.d.ts +2 -2
  119. package/dist/signals/extractor.js +31 -6
  120. package/dist/signals/index.d.ts +3 -1
  121. package/dist/signals/index.js +3 -1
  122. package/dist/signals/metaSignals.d.ts +4 -0
  123. package/dist/signals/metaSignals.js +42 -0
  124. package/dist/signals/signalGate.js +1 -1
  125. package/dist/signals/taskDomain.d.ts +22 -0
  126. package/dist/signals/taskDomain.js +43 -0
  127. package/dist/strategy/constraintAblation.js +115 -369
  128. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  129. package/dist/strategy/constraintAblationPredicates.js +339 -0
  130. package/dist/trace/index.d.ts +3 -1
  131. package/dist/trace/index.js +3 -1
  132. package/dist/trace/learningTrace.d.ts +209 -0
  133. package/dist/trace/learningTrace.js +276 -0
  134. package/dist/trace/proxyTurns.d.ts +31 -0
  135. package/dist/trace/proxyTurns.js +137 -0
  136. package/dist/verify/sandboxRunner.d.ts +28 -0
  137. package/dist/verify/sandboxRunner.js +218 -19
  138. package/dist/verify/sandboxedValidation.d.ts +9 -0
  139. package/dist/verify/sandboxedValidation.js +113 -13
  140. package/dist/verify/validation.d.ts +11 -1
  141. package/dist/verify/validation.js +31 -0
  142. package/package.json +5 -2
@@ -5,7 +5,7 @@ import { TextDecoder } from 'node:util';
5
5
  import { canonicalize } from '../wire/index.js';
6
6
  import { LockReleaseError } from '../util/fileLock.js';
7
7
  import { AssetStoreReadLimitError, assertAssetStoreDirectory, createBufferDurableExclusive, fsyncDirectoryBestEffort, readRegularBuffer, regularFileFingerprint, replaceUtf8Durable, withAssetStoreLock, } from './assetStoreStorage.js';
8
- import { parseAssetSyncRecord, parseProvenanceRecord, parseReviewRecord, } from './assetSidecarRecords.js';
8
+ import { parseAssetSyncSidecarRecord, parseProvenanceRecord, parseReviewRecord, } from './assetSidecarRecords.js';
9
9
  export class AssetSidecarRecoveryError extends Error {
10
10
  reason;
11
11
  code = 'ASSET_SIDECAR_RECOVERY_FAILED';
@@ -29,7 +29,7 @@ const SIDECAR_FILES = {
29
29
  const RECORD_PARSERS = {
30
30
  provenance: parseProvenanceRecord,
31
31
  review: parseReviewRecord,
32
- 'asset-sync': parseAssetSyncRecord,
32
+ 'asset-sync': parseAssetSyncSidecarRecord,
33
33
  };
34
34
  function isErrno(error, code) {
35
35
  return typeof error === 'object' && error !== null && error.code === code;
@@ -15,6 +15,11 @@ export interface AssetStoreFileHealth {
15
15
  duplicateRows: number;
16
16
  corruptRows: number;
17
17
  hashMismatchRows: number;
18
+ /**
19
+ * Rows that fail content-hash verification but carry an active, content-bound unverified provenance waiver.
20
+ * They remain untrusted and are counted separately instead of being misclassified as store corruption.
21
+ */
22
+ unverifiedRows: number;
18
23
  schemaInvalidRows: number;
19
24
  unterminated: boolean;
20
25
  reason?: UnsafeAssetStorePathReason | 'base_directory' | 'lock_unavailable' | 'read_unavailable' | 'scan_limit_exceeded';
@@ -31,6 +36,7 @@ export interface AssetStoreHealthTotals {
31
36
  duplicateRows: number;
32
37
  corruptRows: number;
33
38
  hashMismatchRows: number;
39
+ unverifiedRows: number;
34
40
  schemaInvalidRows: number;
35
41
  unterminatedFiles: number;
36
42
  }
@@ -1,15 +1,16 @@
1
1
  import { lstatSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { acquireLock, releaseLock } from '../util/fileLock.js';
4
- import { validateWire, verifyAssetId } from '../wire/index.js';
4
+ import { computeAssetId, validateWire, verifyAssetId } from '../wire/index.js';
5
5
  import { LOCAL_ASSET_FILES } from './assetStoreLayout.js';
6
6
  import { assertOptionalRegularFile, isReliableAssetStoreLockRelease, readUtf8Regular, UnsafeAssetStorePathError, } from './assetStoreStorage.js';
7
- import { parseAssetSyncRecord, parseProvenanceRecord, parseReviewRecord, parseSidecarJsonl, } from './assetSidecarRecords.js';
7
+ import { isActiveUnverifiedProvenance } from './provenance.js';
8
+ import { parseAssetSyncSidecarRecord, parseProvenanceRecord, parseReviewRecord, parseSidecarJsonl, } from './assetSidecarRecords.js';
8
9
  export const DEFAULT_ASSET_HEALTH_MAX_FILE_BYTES = 64 * 1024 * 1024;
9
10
  const LOCAL_ASSET_SIDECARS = [
10
11
  { kind: 'provenance', file: 'provenance.jsonl', parseRecord: parseProvenanceRecord },
11
12
  { kind: 'review', file: 'review.jsonl', parseRecord: parseReviewRecord },
12
- { kind: 'asset-sync', file: 'asset-sync.jsonl', parseRecord: parseAssetSyncRecord },
13
+ { kind: 'asset-sync', file: 'asset-sync.jsonl', parseRecord: parseAssetSyncSidecarRecord },
13
14
  ];
14
15
  function healthScanLimit(value) {
15
16
  if (value === undefined || !Number.isFinite(value))
@@ -31,6 +32,7 @@ function emptyFile(kind, file, status, reason) {
31
32
  duplicateRows: 0,
32
33
  corruptRows: 0,
33
34
  hashMismatchRows: 0,
35
+ unverifiedRows: 0,
34
36
  schemaInvalidRows: 0,
35
37
  unterminated: false,
36
38
  ...(reason ? { reason } : {}),
@@ -69,6 +71,7 @@ function summarize(files, sidecars) {
69
71
  duplicateRows: files.reduce((sum, file) => sum + file.duplicateRows, 0),
70
72
  corruptRows: files.reduce((sum, file) => sum + file.corruptRows, 0),
71
73
  hashMismatchRows: files.reduce((sum, file) => sum + file.hashMismatchRows, 0),
74
+ unverifiedRows: files.reduce((sum, file) => sum + file.unverifiedRows, 0),
72
75
  schemaInvalidRows: files.reduce((sum, file) => sum + file.schemaInvalidRows, 0),
73
76
  unterminatedFiles: files.filter((file) => file.unterminated).length,
74
77
  };
@@ -105,7 +108,7 @@ function summarize(files, sidecars) {
105
108
  sidecars: [...sidecars],
106
109
  };
107
110
  }
108
- function inspectFile(baseDir, kind, file, maxFileBytes) {
111
+ function inspectFile(baseDir, kind, file, maxFileBytes, unverifiedContentIds) {
109
112
  try {
110
113
  const path = join(baseDir, file);
111
114
  const stat = assertOptionalRegularFile(path);
@@ -114,7 +117,7 @@ function inspectFile(baseDir, kind, file, maxFileBytes) {
114
117
  if (stat.size > maxFileBytes) {
115
118
  return { ...emptyFile(kind, file, 'unavailable', 'scan_limit_exceeded'), bytes: stat.size };
116
119
  }
117
- const raw = readUtf8Regular(path);
120
+ const raw = readUtf8Regular(path, maxFileBytes);
118
121
  if (raw === null)
119
122
  return emptyFile(kind, file, 'unavailable', 'read_unavailable');
120
123
  const rows = raw.split('\n').filter((line) => line.trim().length > 0);
@@ -123,6 +126,7 @@ function inspectFile(baseDir, kind, file, maxFileBytes) {
123
126
  let duplicateRows = 0;
124
127
  let corruptRows = 0;
125
128
  let hashMismatchRows = 0;
129
+ let unverifiedRows = 0;
126
130
  let schemaInvalidRows = 0;
127
131
  for (const line of rows) {
128
132
  try {
@@ -141,15 +145,22 @@ function inspectFile(baseDir, kind, file, maxFileBytes) {
141
145
  duplicateRows += 1;
142
146
  else
143
147
  seen.add(assetId);
144
- if (!verifyAssetId(record)) {
145
- hashMismatchRows += 1;
146
- continue;
148
+ const hashMatches = verifyAssetId(record);
149
+ if (!hashMatches) {
150
+ // A hash mismatch is corruption UNLESS provenance says this id is an unverified hub reuse: the hub
151
+ // rewrote the delivered bytes and reuse froze them untrusted on purpose (#570). That is expected, not
152
+ // store rot, so it lands in the benign unverifiedRows bucket and never degrades the store.
153
+ if (unverifiedContentIds.get(assetId) === computeAssetId(record))
154
+ unverifiedRows += 1;
155
+ else
156
+ hashMismatchRows += 1;
147
157
  }
148
- if (kind !== 'AntiGene' && !validateWire(record).ok) {
158
+ const schemaValid = kind === 'AntiGene' || validateWire(record).ok;
159
+ if (!schemaValid) {
149
160
  schemaInvalidRows += 1;
150
- continue;
151
161
  }
152
- validRows += 1;
162
+ if (hashMatches && schemaValid)
163
+ validRows += 1;
153
164
  }
154
165
  catch {
155
166
  corruptRows += 1;
@@ -174,6 +185,7 @@ function inspectFile(baseDir, kind, file, maxFileBytes) {
174
185
  duplicateRows,
175
186
  corruptRows,
176
187
  hashMismatchRows,
188
+ unverifiedRows,
177
189
  schemaInvalidRows,
178
190
  unterminated,
179
191
  };
@@ -184,6 +196,33 @@ function inspectFile(baseDir, kind, file, maxFileBytes) {
184
196
  return emptyFile(kind, file, 'unavailable', 'read_unavailable');
185
197
  }
186
198
  }
199
+ /**
200
+ * Asset ids whose latest provenance decision marks a supported unverified frozen ingest.
201
+ * Read directly from `<baseDir>/provenance.jsonl` — NOT via ProvenanceStore — because inspectLocalAssetStore
202
+ * already holds the shared `.assetstore.lock`, and ProvenanceStore would try to re-acquire it. A missing or
203
+ * unreadable sidecar yields an empty map: without provenance every mismatch stays classified as corruption.
204
+ */
205
+ function readUnverifiedReuseContentIds(baseDir, maxFileBytes) {
206
+ let raw;
207
+ try {
208
+ raw = readUtf8Regular(join(baseDir, 'provenance.jsonl'), maxFileBytes);
209
+ }
210
+ catch {
211
+ return new Map();
212
+ }
213
+ if (raw === null)
214
+ return new Map();
215
+ const ids = new Map();
216
+ for (const record of parseSidecarJsonl(raw, parseProvenanceRecord).records) {
217
+ const frozenContentId = record.frozenContentId;
218
+ if (frozenContentId && isActiveUnverifiedProvenance(record, frozenContentId)) {
219
+ ids.set(record.assetId, frozenContentId);
220
+ }
221
+ else
222
+ ids.delete(record.assetId);
223
+ }
224
+ return ids;
225
+ }
187
226
  function inspectSidecar(baseDir, definition, maxFileBytes) {
188
227
  const { kind, file, parseRecord } = definition;
189
228
  try {
@@ -194,7 +233,7 @@ function inspectSidecar(baseDir, definition, maxFileBytes) {
194
233
  if (stat.size > maxFileBytes) {
195
234
  return { ...emptySidecar(kind, file, 'unavailable', 'scan_limit_exceeded'), bytes: stat.size };
196
235
  }
197
- const raw = readUtf8Regular(path);
236
+ const raw = readUtf8Regular(path, maxFileBytes);
198
237
  if (raw === null)
199
238
  return emptySidecar(kind, file, 'unavailable', 'read_unavailable');
200
239
  const parsed = parseSidecarJsonl(raw, parseRecord);
@@ -255,11 +294,14 @@ export function inspectLocalAssetStore(baseDir, opts = {}, deps = {}) {
255
294
  catch {
256
295
  return summarize(allFiles('unavailable', 'lock_unavailable'), allSidecars('unavailable', 'lock_unavailable'));
257
296
  }
258
- const maxFileBytes = healthScanLimit(opts.maxFileBytes);
259
297
  let report;
260
298
  try {
299
+ const maxFileBytes = healthScanLimit(opts.maxFileBytes);
300
+ // Read the unverified-reuse set once, under the lock we already hold, so every asset file classifies its
301
+ // hash mismatches consistently against the same bounded provenance snapshot.
302
+ const unverifiedContentIds = readUnverifiedReuseContentIds(baseDir, maxFileBytes);
261
303
  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)));
304
+ .map(([kind, file]) => inspectFile(baseDir, kind, file, maxFileBytes, unverifiedContentIds)), LOCAL_ASSET_SIDECARS.map((definition) => inspectSidecar(baseDir, definition, maxFileBytes)));
263
305
  }
264
306
  catch {
265
307
  report = summarize(allFiles('unavailable', 'read_unavailable'), allSidecars('unavailable', 'read_unavailable'));
@@ -33,7 +33,7 @@ export declare function withAssetStoreLock<T>(lockPath: string, operation: () =>
33
33
  export declare function assertOptionalRegularFile(path: string, role?: AssetStorePathRole): Stats | null;
34
34
  export declare function regularFileFingerprint(path: string): string;
35
35
  export declare function readRegularBuffer(path: string, maxBytes?: number): Buffer | null;
36
- export declare function readUtf8Regular(path: string): string | null;
36
+ export declare function readUtf8Regular(path: string, maxBytes?: number): string | null;
37
37
  export declare function createBufferDurableExclusive(path: string, value: Buffer, opts?: DurableWriteOptions): void;
38
38
  export declare function fsyncDirectoryBestEffort(path: string): void;
39
39
  export declare function appendUtf8Durable(path: string, value: string, opts?: DurableWriteOptions): void;
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, unlinkSync, writeSync, } from 'node:fs';
2
+ import { closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readSync, renameSync, unlinkSync, writeSync, } from 'node:fs';
3
3
  import { basename, dirname, join } from 'node:path';
4
4
  import { acquireLock, LockReleaseError, releaseLock, } from '../util/fileLock.js';
5
5
  export class UnsafeAssetStorePathError extends Error {
@@ -152,17 +152,26 @@ export function readRegularBuffer(path, maxBytes = Number.MAX_SAFE_INTEGER) {
152
152
  assertOpenedPathMatches(fd, path, 'asset_file');
153
153
  if (fstatSync(fd).size > maxBytes)
154
154
  throw new AssetStoreReadLimitError();
155
- const value = readFileSync(fd);
156
- if (value.byteLength > maxBytes)
157
- throw new AssetStoreReadLimitError();
158
- return value;
155
+ const chunks = [];
156
+ let total = 0;
157
+ while (total <= maxBytes) {
158
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes - total + 1));
159
+ const bytesRead = readSync(fd, chunk, 0, chunk.byteLength, null);
160
+ if (bytesRead === 0)
161
+ break;
162
+ total += bytesRead;
163
+ if (total > maxBytes)
164
+ throw new AssetStoreReadLimitError();
165
+ chunks.push(bytesRead === chunk.byteLength ? chunk : chunk.subarray(0, bytesRead));
166
+ }
167
+ return Buffer.concat(chunks, total);
159
168
  }
160
169
  finally {
161
170
  closeSync(fd);
162
171
  }
163
172
  }
164
- export function readUtf8Regular(path) {
165
- return readRegularBuffer(path)?.toString('utf8') ?? null;
173
+ export function readUtf8Regular(path, maxBytes = Number.MAX_SAFE_INTEGER) {
174
+ return readRegularBuffer(path, maxBytes)?.toString('utf8') ?? null;
166
175
  }
167
176
  function writeAll(fd, value) {
168
177
  const bytes = typeof value === 'string' ? Buffer.from(value, 'utf8') : value;
@@ -237,10 +246,19 @@ export function appendUtf8Durable(path, value, opts = {}) {
237
246
  const parent = dirname(path);
238
247
  assertAssetStoreDirectory(parent);
239
248
  const existed = assertOptionalRegularFile(path) !== null;
240
- const fd = openNoFollow(path, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT, 0o600);
249
+ const fd = openNoFollow(path, constants.O_RDWR | constants.O_APPEND | constants.O_CREAT, 0o600);
241
250
  try {
242
251
  assertOpenedPathMatches(fd, path, 'asset_file');
243
- writeAll(fd, value);
252
+ const stat = fstatSync(fd);
253
+ let needsLineSeparator = false;
254
+ if (stat.size > 0 && value.length > 0) {
255
+ const tail = Buffer.allocUnsafe(1);
256
+ const bytesRead = readSync(fd, tail, 0, 1, stat.size - 1);
257
+ if (bytesRead !== 1)
258
+ throw new Error('asset store tail read made no progress');
259
+ needsLineSeparator = tail[0] !== 0x0a;
260
+ }
261
+ writeAll(fd, needsLineSeparator ? `\n${value}` : value);
244
262
  (opts.syncFile ?? fsyncSync)(fd);
245
263
  }
246
264
  finally {
@@ -1,4 +1,8 @@
1
1
  import type { AssetKind } from './provider.js';
2
+ export declare const ASSET_SYNC_INVENTORY_MAX_SEGMENTS = 24;
3
+ export declare const ASSET_SYNC_INVENTORY_MAX_ITEMS_PER_SEGMENT = 10000;
4
+ export declare const ASSET_SYNC_INVENTORY_MAX_UNIQUE_ITEMS = 240000;
5
+ export declare const ASSET_SYNC_INVENTORY_MAX_TOTAL_BYTES: number;
2
6
  export type AssetSyncSource = 'hub';
3
7
  export type AssetSyncScope = 'purchased' | 'published';
4
8
  export interface AssetSyncRecord {
@@ -8,25 +12,107 @@ export interface AssetSyncRecord {
8
12
  scope: AssetSyncScope;
9
13
  syncedAt: string;
10
14
  remoteAssetId: string;
15
+ /** Opaque sync identity hash. Legacy records omit it and are not used for identity-scoped reconciliation. */
16
+ runKey?: string;
17
+ /** Parameter-independent remote inventory identity used for reconciliation across sync runs. */
18
+ inventoryKey?: string;
11
19
  logicalId?: string;
12
20
  status?: string;
13
21
  forced?: true;
14
22
  collisionWithAssetId?: string;
15
23
  }
24
+ export type AssetSyncRunState = 'started' | 'progress' | 'completed';
25
+ export type AssetSyncRunOutcome = 'imported' | 'already_local' | 'failed' | 'remote_missing';
26
+ export interface AssetSyncRunRecord {
27
+ recordType: 'run';
28
+ runId: string;
29
+ runKey: string;
30
+ state: AssetSyncRunState;
31
+ remoteAssetId?: string;
32
+ outcome?: AssetSyncRunOutcome;
33
+ reason?: string;
34
+ /** Ordered candidate IDs selected when this run was started. */
35
+ plan?: readonly string[];
36
+ syncedAt: string;
37
+ }
38
+ export interface AssetSyncRunSnapshot extends AssetSyncRunRecord {
39
+ readonly processed: ReadonlyMap<string, AssetSyncRunRecord>;
40
+ }
41
+ export type AssetSyncInventoryOutcome = 'imported' | 'already_local' | 'blocked' | 'failed' | 'pending';
42
+ export interface AssetSyncInventoryCursorFingerprints {
43
+ readonly purchased: string | null;
44
+ readonly published: string | null;
45
+ }
46
+ export interface AssetSyncInventoryItem {
47
+ readonly remoteAssetId: string;
48
+ readonly outcome: AssetSyncInventoryOutcome;
49
+ }
50
+ export interface AssetSyncInventorySegmentRecord {
51
+ readonly recordType: 'inventory_scan';
52
+ readonly scanId: string;
53
+ readonly inventoryKey: string;
54
+ readonly scope: 'purchased' | 'published' | 'all';
55
+ readonly index: number;
56
+ readonly inputCursorFingerprints: AssetSyncInventoryCursorFingerprints;
57
+ readonly nextCursorFingerprints: AssetSyncInventoryCursorFingerprints;
58
+ readonly items: readonly AssetSyncInventoryItem[];
59
+ readonly anonymousBlocked: number;
60
+ /** This segment must be replayed from its input cursor before the scan may advance. */
61
+ readonly cursorHeld?: true;
62
+ /** Multi-segment batches are applied only after every physical segment is present. */
63
+ readonly batchId?: string;
64
+ readonly batchIndex?: number;
65
+ readonly batchSize?: number;
66
+ readonly syncedAt: string;
67
+ }
68
+ export type AssetSyncInventoryBatch = Omit<AssetSyncInventorySegmentRecord, 'recordType' | 'syncedAt' | 'batchId' | 'batchIndex' | 'batchSize'> & {
69
+ syncedAt?: string;
70
+ };
71
+ export interface AssetSyncInventorySnapshot {
72
+ readonly scanId: string;
73
+ readonly inventoryKey: string;
74
+ readonly scope: 'purchased' | 'published' | 'all';
75
+ readonly segmentCount: number;
76
+ readonly nextCursorFingerprints: AssetSyncInventoryCursorFingerprints;
77
+ readonly outcomes: ReadonlyMap<string, AssetSyncInventoryOutcome>;
78
+ readonly anonymousBlocked: number;
79
+ readonly complete: boolean;
80
+ /** Physical segment index whose input cursor must be replayed before this scan can advance. */
81
+ readonly retryIndex?: number;
82
+ }
16
83
  export declare class AssetSyncLedger {
17
84
  private readonly now;
18
85
  private readonly path;
19
86
  private readonly lockPath;
20
87
  private readonly index;
88
+ private readonly runKeyIndex;
89
+ private readonly inventoryKeyIndex;
21
90
  private fileState;
22
91
  private loaded;
23
92
  constructor(baseDir: string, now?: () => number);
24
93
  append(rec: Omit<AssetSyncRecord, 'syncedAt'> & {
25
94
  syncedAt?: string;
26
95
  }): AssetSyncRecord;
96
+ appendRun(rec: Omit<AssetSyncRunRecord, 'recordType' | 'syncedAt'> & {
97
+ syncedAt?: string;
98
+ }): AssetSyncRunRecord;
99
+ appendInventorySegment(rec: Omit<AssetSyncInventorySegmentRecord, 'recordType' | 'syncedAt'> & {
100
+ syncedAt?: string;
101
+ }): AssetSyncInventorySegmentRecord | null;
102
+ appendInventoryBatch(rec: AssetSyncInventoryBatch): readonly AssetSyncInventorySegmentRecord[] | null;
103
+ replaceInventoryRetryBatch(rec: AssetSyncInventoryBatch): readonly AssetSyncInventorySegmentRecord[] | null;
104
+ clearInventoryScan(inventoryKey: string): void;
105
+ runRecords(runId: string): AssetSyncRunRecord[];
106
+ latestIncompleteRun(runKey: string): AssetSyncRunSnapshot | undefined;
107
+ latestInventoryScan(inventoryKey: string): AssetSyncInventorySnapshot | undefined;
27
108
  get(assetId: string): AssetSyncRecord | null;
109
+ getForRunKey(runKey: string, assetId: string): AssetSyncRecord | null;
28
110
  list(): AssetSyncRecord[];
111
+ listForRunKey(runKey: string): AssetSyncRecord[];
112
+ listForInventoryKey(inventoryKey: string): AssetSyncRecord[];
29
113
  private rebuildIndex;
30
114
  private refreshUnderLock;
31
115
  private withFreshRead;
116
+ private readRunRecordsUnderLock;
117
+ private latestInventorySnapshotUnderLock;
32
118
  }