@lmzhen/dsh-evolution-state-json 0.17.0 → 0.18.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.
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { evolutionHome, makeSerialQueue, transactIo, transactTaskGuard } from "@lmzhen/dsh-evolution-core";
2
+ import { evolutionHome, isAbsent, isPresent, isUnknown, makeSerialQueue, probeText, transactIo, transactTaskGuard } from "@lmzhen/dsh-evolution-core";
3
3
  import { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_TABLE, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_SESSION_CAP, REVIEW_STATE_TABLE, assertCloneable, canClaimPending, canResolvePending, cloneRecord, recordIssue, releasedStatus, selectPendingOverflow, selectSessionOverflow } from "@lmzhen/dsh-evolution-state-storage";
4
4
  import { basename, dirname, isAbsolute, join } from "node:path";
5
5
  //#region lib/types/index.js
@@ -68,11 +68,16 @@ const QUARANTINE_ERROR_NAME = "EvolutionStateCorruptFile";
68
68
  * DIFFERENT payload gets a stamped sibling so the earlier rescue copy is never
69
69
  * overwritten. The node backend's 7-day `.corrupt` sweep bounds the set. */
70
70
  async function quarantineTarget(io, base, content) {
71
- if (!await io().exists(base).catch(() => false)) return {
71
+ const baseProbe = await probeText(io(), base);
72
+ if (isUnknown(baseProbe)) return {
73
+ dest: `${base}.${Date.now()}`,
74
+ needsWrite: true
75
+ };
76
+ if (isAbsent(baseProbe)) return {
72
77
  dest: base,
73
78
  needsWrite: true
74
79
  };
75
- if (await io().readText(base).catch(() => null) === content) return {
80
+ if (baseProbe.value === content) return {
76
81
  dest: base,
77
82
  needsWrite: false
78
83
  };
@@ -83,7 +88,8 @@ async function quarantineTarget(io, base, content) {
83
88
  if (!name.startsWith(stem)) continue;
84
89
  if (!/^\d+$/.test(name.slice(stem.length))) continue;
85
90
  const sibling = join(dir, name);
86
- if (await io().readText(sibling).catch(() => null) === content) return {
91
+ const siblingProbe = await probeText(io(), sibling);
92
+ if (isPresent(siblingProbe) && siblingProbe.value === content) return {
87
93
  dest: sibling,
88
94
  needsWrite: false
89
95
  };
@@ -94,16 +100,16 @@ async function quarantineTarget(io, base, content) {
94
100
  needsWrite: true
95
101
  };
96
102
  }
97
- async function quarantine(io, root, file, raw, reason) {
103
+ async function quarantine(io, root, file, raw, reason, memory) {
98
104
  const { dest, needsWrite } = await quarantineTarget(io, `${join(root, file)}.corrupt`, raw);
99
105
  let preservedNote = `; original preserved at ${dest} — inspect and fix it, then retry.`;
100
106
  if (needsWrite) try {
101
107
  await io().writeText(dest, raw);
102
- corruptWritten.delete(file);
108
+ memory.written.delete(file);
103
109
  } catch (writeError) {
104
110
  preservedNote = `; the quarantine copy at ${dest} FAILED to write (${writeError instanceof Error ? writeError.message : String(writeError)}) — the original file is left in place.`;
105
111
  }
106
- else corruptWritten.delete(file);
112
+ else memory.written.delete(file);
107
113
  throw Object.assign(/* @__PURE__ */ new Error(`evolution state file "${file}" is not valid JSON (${reason})${preservedNote}`), { name: QUARANTINE_ERROR_NAME });
108
114
  }
109
115
  /** S-05: the V8-16 record-shape gate existed as two verbatim ~15-line
@@ -132,22 +138,14 @@ function gateScan(file, parsed) {
132
138
  if (gate === void 0 || !isPlainRecord(parsed)) return [];
133
139
  return Object.entries(parsed).filter(([, record]) => !isPlainRecord(record) || !gate(record));
134
140
  }
135
- /**
136
- * Cross-process JSON-file RMW (v3-audit M-8): every read-modify-write state
137
- * mutation runs inside the IO backend's transact lock (via transactIo) so a
138
- * second process sharing DSH_HOME cannot interleave its claim/resolve.
139
- * `task` returns the next value; null = ENSURE-ABSENT (the file is deleted
140
- * when it exists — the parity of the transact seam, V6-32). For a record-map
141
- * file the return must be null or a plain object map of records, and an
142
- * array/scalar would be persisted as a corrupt map — so it fails loud before
143
- * any write (0.3.28, V4-08). The legacy `pending.json` merge stays inside the
144
- * task via `readJson` where relevant.
145
- * @internal Exported only for this package's own tests — not public API
146
- * surface (audit v10 S-03); other packages must go through the provider seam.
147
- */
148
- const recordGateWarned = /* @__PURE__ */ new Set();
149
- const corruptWritten = /* @__PURE__ */ new Map();
150
- const corruptWriteWarned = /* @__PURE__ */ new Set();
141
+ /** One provider instance's quarantine memory. */
142
+ function newQuarantineMemory() {
143
+ return {
144
+ warned: /* @__PURE__ */ new Set(),
145
+ written: /* @__PURE__ */ new Map(),
146
+ writeWarned: /* @__PURE__ */ new Set()
147
+ };
148
+ }
151
149
  /** P3 (v15): name carried by the two fail-loud write gates in `jsonTransact`
152
150
  * (task-return shape, write-back field gate) so best-effort wrappers —
153
151
  * `retireLegacyOnce` is the one that matters — rethrow instead of deferring. */
@@ -157,26 +155,26 @@ function writeGateError(message) {
157
155
  error.name = WRITE_GATE_ERROR_NAME;
158
156
  return error;
159
157
  }
160
- function reportGateViolation(ctx, file, failing) {
161
- if (recordGateWarned.has(file)) return;
162
- recordGateWarned.add(file);
158
+ function reportGateViolation(ctx, file, failing, memory) {
159
+ if (memory.warned.has(file)) return;
160
+ memory.warned.add(file);
163
161
  ctx.logger.warn(`evolution-state-json: ${failing.length} record(s) in "${file}" failed the record schema gate and were quarantined to "${file}.corrupt": ${failing.map(([id]) => id).join(", ")}`);
164
162
  }
165
- async function ensureCorruptCopy(ctx, io, root, file, bad) {
163
+ async function ensureCorruptCopy(ctx, io, root, file, bad, memory) {
166
164
  const base = `${join(root, file)}.corrupt`;
167
165
  const corruptKey = JSON.stringify(Object.entries(bad).map(([id, record]) => ({
168
166
  id,
169
167
  fields: typeof record === "object" && record !== null ? Object.entries(record).map(([field, value]) => `${field}:${Array.isArray(value) ? "array" : typeof value}`).sort() : [typeof record]
170
168
  })).sort((a, b) => a.id.localeCompare(b.id)));
171
- if (corruptWritten.get(file) === corruptKey && await io().exists(base)) return true;
169
+ if (memory.written.get(file) === corruptKey && await io().exists(base)) return true;
172
170
  const payload = JSON.stringify(bad, null, 2);
173
171
  const { dest, needsWrite } = await quarantineTarget(io, base, payload);
174
172
  const wrote = needsWrite ? await io().writeText(dest, payload).then(() => true).catch(() => false) : true;
175
173
  if (wrote) {
176
- corruptWritten.set(file, corruptKey);
177
- corruptWriteWarned.delete(file);
178
- } else if (!corruptWriteWarned.has(file)) {
179
- corruptWriteWarned.add(file);
174
+ memory.written.set(file, corruptKey);
175
+ memory.writeWarned.delete(file);
176
+ } else if (!memory.writeWarned.has(file)) {
177
+ memory.writeWarned.add(file);
180
178
  ctx.logger.warn(`evolution-state-json: could not write quarantine copy "${dest}" for ${Object.keys(bad).length} failed record(s) — the main file keeps them; the copy is retried on the next access`);
181
179
  }
182
180
  return wrote;
@@ -188,26 +186,26 @@ async function jsonTransact(ctx, io, root, file, task, options) {
188
186
  try {
189
187
  parsed = JSON.parse(current);
190
188
  } catch (error) {
191
- return await quarantine(io, root, file, current, error instanceof Error ? error.message : String(error));
189
+ return await quarantine(io, root, file, current, error instanceof Error ? error.message : String(error), options.memory);
192
190
  }
193
- if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(io, root, file, current, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
191
+ if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(io, root, file, current, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`, options.memory);
194
192
  const malformed = firstNonRecordValue(parsed);
195
- if (malformed !== null) return await quarantine(io, root, file, current, malformed);
193
+ if (malformed !== null) return await quarantine(io, root, file, current, malformed, options.memory);
196
194
  const failing = gateScan(file, parsed);
197
195
  if (failing.length > 0) {
198
196
  const bad = {};
199
197
  const good = {};
200
198
  for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
201
199
  else good[id] = record;
202
- reportGateViolation(ctx, file, failing);
203
- if (await ensureCorruptCopy(ctx, io, root, file, bad)) {
200
+ reportGateViolation(ctx, file, failing, options.memory);
201
+ if (await ensureCorruptCopy(ctx, io, root, file, bad, options.memory)) {
204
202
  parsed = good;
205
- options?.onGateDrop?.(file, failing.map(([id]) => id));
203
+ options.onGateDrop?.(file, failing.map(([id]) => id));
206
204
  } else {
207
- options?.onGateDrop?.(file, []);
205
+ options.onGateDrop?.(file, []);
208
206
  ctx.logger.warn(`evolution-state-json: keeping ${failing.length} malformed record(s) in ${file} — the quarantine copy could not be written, so rewriting the file without them would destroy the only copy`);
209
207
  }
210
- } else options?.onGateDrop?.(file, []);
208
+ } else options.onGateDrop?.(file, []);
211
209
  }
212
210
  const next = await task(parsed);
213
211
  if (next !== null && RECORD_MAP_FILES.has(file) && !isPlainRecord(next)) throw writeGateError(`evolution state file "${file}" task returned ${Array.isArray(next) ? "an array" : typeof next} (expected null or a plain JSON object map of records); not written.`);
@@ -223,6 +221,7 @@ function apply(ctx, rawConfig = {}) {
223
221
  if (rawConfig.root !== void 0 && rawConfig.root.trim() !== "" && !isAbsolute(rawConfig.root)) ctx.logger.warn(`evolution-state-json: config.root "${rawConfig.root}" is a relative path and resolves against the process CWD ("${root}") — pass an absolute path to make the store location launch-independent`);
224
222
  const io = () => ctx.evolutionIo.provider();
225
223
  const pathOf = (file) => join(root, file);
224
+ const quarantineMemory = newQuarantineMemory();
226
225
  const gateDroppedCurrent = /* @__PURE__ */ new Set();
227
226
  const noteGateDrop = (file, ids) => {
228
227
  if (file !== PENDING_STATE_FILE) return;
@@ -242,7 +241,16 @@ function apply(ctx, rawConfig = {}) {
242
241
  * @param task - the read-modify-write body, exactly as jsonTransact takes it.
243
242
  * @returns jsonTransact's own result.
244
243
  */
245
- const pendingTransact = (task) => jsonTransact(ctx, io, root, PENDING_STATE_FILE, task, { onGateDrop: noteGateDrop });
244
+ const pendingTransact = (task, what = "the pending table") => {
245
+ const guard = transactTaskGuard(`pending table write for ${what}`);
246
+ const pendingWrite = {
247
+ onGateDrop: noteGateDrop,
248
+ memory: quarantineMemory
249
+ };
250
+ return jsonTransact(ctx, io, root, PENDING_STATE_FILE, guard.wrap(task), pendingWrite).then(() => {
251
+ guard.assertInvoked();
252
+ });
253
+ };
246
254
  let warnedPendingCapacity = false;
247
255
  let warnedPendingArgsBytes = false;
248
256
  async function readJson(file) {
@@ -252,19 +260,19 @@ function apply(ctx, rawConfig = {}) {
252
260
  try {
253
261
  parsed = JSON.parse(raw);
254
262
  } catch (error) {
255
- return await quarantine(io, root, file, raw, error instanceof Error ? error.message : String(error));
263
+ return await quarantine(io, root, file, raw, error instanceof Error ? error.message : String(error), quarantineMemory);
256
264
  }
257
- if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(io, root, file, raw, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
265
+ if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(io, root, file, raw, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`, quarantineMemory);
258
266
  const malformed = firstNonRecordValue(parsed);
259
- if (malformed !== null) return await quarantine(io, root, file, raw, malformed);
267
+ if (malformed !== null) return await quarantine(io, root, file, raw, malformed, quarantineMemory);
260
268
  const failing = gateScan(file, parsed);
261
269
  if (failing.length > 0) {
262
270
  const bad = {};
263
271
  const good = {};
264
272
  for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
265
273
  else good[id] = record;
266
- reportGateViolation(ctx, file, failing);
267
- await ensureCorruptCopy(ctx, io, root, file, bad);
274
+ reportGateViolation(ctx, file, failing, quarantineMemory);
275
+ await ensureCorruptCopy(ctx, io, root, file, bad, quarantineMemory);
268
276
  if (file === PENDING_STATE_FILE) {
269
277
  gateDroppedCurrent.clear();
270
278
  for (const id of Object.keys(bad)) gateDroppedCurrent.add(id);
@@ -362,6 +370,7 @@ function apply(ctx, rawConfig = {}) {
362
370
  async function retireLegacyOnce(legacy) {
363
371
  if (legacyMigrated) return {};
364
372
  const keyedLegacy = keyPendingById(legacy);
373
+ let committed = null;
365
374
  try {
366
375
  const retired = {};
367
376
  await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (fresh) => {
@@ -374,15 +383,20 @@ function apply(ctx, rawConfig = {}) {
374
383
  merged[id] = record;
375
384
  retired[id] = record;
376
385
  }
386
+ committed = merged;
377
387
  return merged;
378
- }, { onGateDrop: noteGateDrop });
388
+ }, {
389
+ onGateDrop: noteGateDrop,
390
+ memory: quarantineMemory
391
+ });
379
392
  await io().rename(pathOf(PENDING_LEGACY_FILE), `${pathOf(PENDING_LEGACY_FILE)}.migrated`);
380
393
  legacyMigrated = true;
381
394
  return retired;
382
395
  } catch (error) {
383
396
  if (error instanceof Error && (error.name === QUARANTINE_ERROR_NAME || error.name === WRITE_GATE_ERROR_NAME)) throw error;
384
397
  ctx.logger.warn(`evolution-state-json: legacy pending retirement deferred: ${error instanceof Error ? error.message : String(error)}`);
385
- return {};
398
+ if (committed === null) return {};
399
+ return committed;
386
400
  }
387
401
  }
388
402
  async function loadPendingMap() {
@@ -449,9 +463,10 @@ function apply(ctx, rawConfig = {}) {
449
463
  * revive a ghost twin. A dedupe no-op counts as success — the records are
450
464
  * already in the archive. */
451
465
  async function appendArchive(records) {
466
+ const guard = transactTaskGuard(`pending archive (${PENDING_ARCHIVE_FILE})`);
452
467
  let landed = true;
453
468
  try {
454
- await transactIo(io(), pathOf(PENDING_ARCHIVE_FILE), async (current) => {
469
+ await transactIo(io(), pathOf(PENDING_ARCHIVE_FILE), guard.wrap(async (current) => {
455
470
  let archive = [];
456
471
  if (current !== null) {
457
472
  let parsed;
@@ -488,7 +503,8 @@ function apply(ctx, rawConfig = {}) {
488
503
  return JSON.stringify((fresh.length > 0 ? fresh : next).slice(-5e3));
489
504
  }
490
505
  return JSON.stringify(next);
491
- });
506
+ }));
507
+ if (!guard.invoked()) landed = false;
492
508
  return landed;
493
509
  } catch (error) {
494
510
  ctx.logger.warn(`evolution-state-json: archive append deferred: ${error instanceof Error ? error.message : String(error)}`);
@@ -549,7 +565,7 @@ function apply(ctx, rawConfig = {}) {
549
565
  const pruned = {};
550
566
  for (const [id, row] of Object.entries(stamped)) if (!evict.has(id)) pruned[id] = row;
551
567
  return pruned;
552
- }));
568
+ }), { memory: quarantineMemory });
553
569
  guard.assertInvoked();
554
570
  });
555
571
  },
@@ -564,7 +580,7 @@ function apply(ctx, rawConfig = {}) {
564
580
  await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, guard.wrap((current) => ({
565
581
  ...current ?? {},
566
582
  [CURATOR_STATE_KEY]: record
567
- })));
583
+ })), { memory: quarantineMemory });
568
584
  guard.assertInvoked();
569
585
  });
570
586
  },
@@ -579,7 +595,7 @@ function apply(ctx, rawConfig = {}) {
579
595
  ...current ?? {},
580
596
  [CURATOR_STATE_KEY]: next
581
597
  };
582
- }));
598
+ }), { memory: quarantineMemory });
583
599
  guard.assertInvoked();
584
600
  });
585
601
  },
@@ -591,16 +607,14 @@ function apply(ctx, rawConfig = {}) {
591
607
  },
592
608
  async savePending(record) {
593
609
  await mutate(async () => {
594
- const guard = transactTaskGuard(`pending record "${record.id}" (${PENDING_STATE_FILE})`);
595
- await pendingTransact(guard.wrap(async (current) => {
610
+ await pendingTransact(async (current) => {
596
611
  const map = {
597
612
  ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readLegacyPending(), current ?? {}),
598
613
  [record.id]: record
599
614
  };
600
615
  warnPendingGrowth(map, "save");
601
616
  return map;
602
- }));
603
- guard.assertInvoked();
617
+ }, `record "${record.id}" (${PENDING_STATE_FILE})`);
604
618
  });
605
619
  },
606
620
  async claimPending(id, claimId) {
@@ -692,4 +706,4 @@ function apply(ctx, rawConfig = {}) {
692
706
  ctx.effect(() => ctx.evolutionStateStorage.registerProvider(provider), "evolution-state-json.provider");
693
707
  }
694
708
  //#endregion
695
- export { Config, apply, inject, jsonTransact, name };
709
+ export { Config, apply, inject, jsonTransact, name, newQuarantineMemory };
@@ -14,8 +14,47 @@ export interface Config {
14
14
  root?: string;
15
15
  }
16
16
  export declare const Config: z<Config>;
17
- export declare function jsonTransact<T>(ctx: Context, io: () => EvolutionIoLike, root: string, file: string, task: (current: T | null) => T | null | Promise<T | null>, options?: {
17
+ /**
18
+ * Cross-process JSON-file RMW (v3-audit M-8): every read-modify-write state
19
+ * mutation runs inside the IO backend's transact lock (via transactIo) so a
20
+ * second process sharing DSH_HOME cannot interleave its claim/resolve.
21
+ * `task` returns the next value; null = ENSURE-ABSENT (the file is deleted
22
+ * when it exists — the parity of the transact seam, V6-32). For a record-map
23
+ * file the return must be null or a plain object map of records, and an
24
+ * array/scalar would be persisted as a corrupt map — so it fails loud before
25
+ * any write (0.3.28, V4-08). The legacy `pending.json` merge stays inside the
26
+ * task via `readJson` where relevant.
27
+ * @internal Exported only for this package's own tests — not public API
28
+ * surface (audit v10 S-03); other packages must go through the provider seam.
29
+ */
30
+ /**
31
+ * The quarantine bookkeeping ONE provider instance owns (N12/O-6, S4.4).
32
+ *
33
+ * These three used to be module-scope ("`jsonTransact` is a module function that cannot reach the
34
+ * apply-scoped closure"), which made warn/rewrite dedup span instances AND roots keyed by file NAME
35
+ * only: two providers on different roots deduped each other's quarantine writes. Threading the memory
36
+ * through the one module function that needs it removes that limit — the dedup is now exactly "this
37
+ * instance, this file", which is what the three fields always described.
38
+ */
39
+ export interface QuarantineMemory {
40
+ /** V10-04 (P2-19): the record-gate warn fires once per file per instance — a permanently bad
41
+ * record would otherwise warn on every turn's read. */
42
+ readonly warned: Set<string>;
43
+ /** P2-24 (v11): the last `.corrupt` rewrite key per file (sorted failing id set) — skip
44
+ * rewriting when nothing changed, so a permanently bad record never re-atomic-writes the copy on
45
+ * every read. */
46
+ readonly written: Map<string, string>;
47
+ /** P3-12 (v14): the "could not write the quarantine copy" warn fires once per file; the key is set
48
+ * ONLY after a successful write (a failed write is retried, not marked done). */
49
+ readonly writeWarned: Set<string>;
50
+ }
51
+ /** One provider instance's quarantine memory. */
52
+ export declare function newQuarantineMemory(): QuarantineMemory;
53
+ export declare function jsonTransact<T>(ctx: Context, io: () => EvolutionIoLike, root: string, file: string, task: (current: T | null) => T | null | Promise<T | null>, options: {
18
54
  onGateDrop?: (file: string, ids: string[]) => void;
55
+ /** N12/O-6 (S4.4): the calling INSTANCE's quarantine memory — one per provider, never module
56
+ * scope (that keyed the dedup by bare file name across every root in the process). */
57
+ memory: QuarantineMemory;
19
58
  }): Promise<void>;
20
59
  export declare function apply(ctx: Context, rawConfig?: Config): void;
21
60
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-state-json",
3
3
  "description": "JSON-file evolution state provider over the IO seam (community build)",
4
- "version": "0.17.0",
4
+ "version": "0.18.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -26,17 +26,17 @@
26
26
  ],
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-core": "^0.17.0"
29
+ "@deepseek-ai/schemastery": "^3.18.4",
30
+ "@lmzhen/dsh-evolution-core": "^0.18.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
- "@lmzhen/dsh-evolution-io": "^0.17.0",
35
- "@lmzhen/dsh-evolution-state-storage": "^0.17.0"
34
+ "@lmzhen/dsh-evolution-io": "^0.18.0",
35
+ "@lmzhen/dsh-evolution-state-storage": "^0.18.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@lmzhen/dsh-evolution-io": "^0.17.0",
39
- "@lmzhen/dsh-evolution-state-storage": "^0.17.0",
40
- "@lmzhen/dsh-evolution-io-node": "^0.17.0"
38
+ "@lmzhen/dsh-evolution-io": "^0.18.0",
39
+ "@lmzhen/dsh-evolution-state-storage": "^0.18.0",
40
+ "@lmzhen/dsh-evolution-io-node": "^0.18.0"
41
41
  }
42
42
  }