@lmzhen/dsh-evolution-state-json 0.3.68 → 0.3.70

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 (2) hide show
  1. package/lib/index.js +73 -23
  2. package/package.json +9 -9
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { evolutionHome, makeSerialQueue, transactIo } 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, recordIssue, releasedStatus, selectPendingOverflow, selectSessionOverflow } from "@lmzhen/dsh-evolution-state-storage";
4
- import { isAbsolute, join } from "node:path";
4
+ import { basename, dirname, isAbsolute, join } from "node:path";
5
5
  //#region lib/types/index.js
6
6
  /**
7
7
  * JSON-file evolution state provider over the IO seam.
@@ -62,18 +62,42 @@ const QUARANTINE_ERROR_NAME = "EvolutionStateCorruptFile";
62
62
  * DIFFERENT payload gets a stamped sibling so the earlier rescue copy is never
63
63
  * overwritten. The node backend's 7-day `.corrupt` sweep bounds the set. */
64
64
  async function quarantineTarget(io, base, content) {
65
- if (!await io().exists(base).catch(() => false)) return base;
66
- return await io().readText(base).catch(() => null) === content ? base : `${base}.${Date.now()}`;
65
+ if (!await io().exists(base).catch(() => false)) return {
66
+ dest: base,
67
+ needsWrite: true
68
+ };
69
+ if (await io().readText(base).catch(() => null) === content) return {
70
+ dest: base,
71
+ needsWrite: false
72
+ };
73
+ const dir = dirname(base);
74
+ const stem = `${basename(base)}.`;
75
+ try {
76
+ for (const name of await io().list(dir)) {
77
+ if (!name.startsWith(stem)) continue;
78
+ if (!/^\d+$/.test(name.slice(stem.length))) continue;
79
+ const sibling = join(dir, name);
80
+ if (await io().readText(sibling).catch(() => null) === content) return {
81
+ dest: sibling,
82
+ needsWrite: false
83
+ };
84
+ }
85
+ } catch {}
86
+ return {
87
+ dest: `${base}.${Date.now()}`,
88
+ needsWrite: true
89
+ };
67
90
  }
68
91
  async function quarantine(io, root, file, raw, reason) {
69
- const dest = await quarantineTarget(io, `${join(root, file)}.corrupt`, raw);
92
+ const { dest, needsWrite } = await quarantineTarget(io, `${join(root, file)}.corrupt`, raw);
70
93
  let preservedNote = `; original preserved at ${dest} — inspect and fix it, then retry.`;
71
- try {
94
+ if (needsWrite) try {
72
95
  await io().writeText(dest, raw);
73
96
  corruptWritten.delete(file);
74
97
  } catch (writeError) {
75
98
  preservedNote = `; the quarantine copy at ${dest} FAILED to write (${writeError instanceof Error ? writeError.message : String(writeError)}) — the original file is left in place.`;
76
99
  }
100
+ else corruptWritten.delete(file);
77
101
  throw Object.assign(/* @__PURE__ */ new Error(`evolution state file "${file}" is not valid JSON (${reason})${preservedNote}`), { name: QUARANTINE_ERROR_NAME });
78
102
  }
79
103
  /** S-05: the V8-16 record-shape gate existed as two verbatim ~15-line
@@ -140,8 +164,8 @@ async function ensureCorruptCopy(ctx, io, root, file, bad) {
140
164
  })).sort((a, b) => a.id.localeCompare(b.id)));
141
165
  if (corruptWritten.get(file) === corruptKey && await io().exists(base)) return true;
142
166
  const payload = JSON.stringify(bad, null, 2);
143
- const dest = await quarantineTarget(io, base, payload);
144
- const wrote = await io().writeText(dest, payload).then(() => true).catch(() => false);
167
+ const { dest, needsWrite } = await quarantineTarget(io, base, payload);
168
+ const wrote = needsWrite ? await io().writeText(dest, payload).then(() => true).catch(() => false) : true;
145
169
  if (wrote) {
146
170
  corruptWritten.set(file, corruptKey);
147
171
  corruptWriteWarned.delete(file);
@@ -290,8 +314,10 @@ function apply(ctx, rawConfig = {}) {
290
314
  };
291
315
  }
292
316
  /** 0.3.22 (F-336): when the live pending map holds more than
293
- * `PENDING_RESOLVED_CAP` resolved records, drop the oldest (by resolvedAt,
294
- * then insertion order on ties) from the map and return them for archiving.
317
+ * `PENDING_RESOLVED_CAP` resolved records, drop the oldest (by resolvedAt;
318
+ * a missing/unparseable timestamp sorts LAST and leaves first only when the
319
+ * overflow exceeds every known timestamp — v28 G2.4 wording, shared seam
320
+ * rule) from the map and return them for archiving.
295
321
  * Only approved/rejected records are candidates — pending/executing are
296
322
  * live work and are never trimmed. Returns the pruned map (rather than
297
323
  * mutating in place) plus the evicted records. */
@@ -319,19 +345,34 @@ function apply(ctx, rawConfig = {}) {
319
345
  * read-only legacy `pending.json` re-introduces an evicted record on the
320
346
  * next resolve) and rotate the sidecar to `.bak` past ARCHIVE_RESOLVED_CAP
321
347
  * so neither the file nor the per-append full-array rewrite grows without
322
- * bound. */
348
+ * bound.
349
+ * v28 G0.2 (STATE-01): returns whether the records' resolved evidence
350
+ * LANDED. `false` (rescue-skip or write failure) hands the caller the
351
+ * replay-window responsibility: the evicted records are no longer in the
352
+ * live map and their only durable "resolved" copy failed to land, so the
353
+ * caller must compensate (merge them back) before the legacy merge can
354
+ * revive a ghost twin. A dedupe no-op counts as success — the records are
355
+ * already in the archive. */
323
356
  async function appendArchive(records) {
357
+ let landed = true;
324
358
  try {
325
359
  await transactIo(io(), pathOf(PENDING_ARCHIVE_FILE), async (current) => {
326
360
  let archive = [];
327
- if (current !== null) try {
328
- const parsed = JSON.parse(current);
329
- if (Array.isArray(parsed)) archive = parsed;
330
- } catch {
331
- if (!await io().writeText(`${pathOf(PENDING_ARCHIVE_FILE)}.corrupt`, current).then(() => true, () => false)) {
332
- ctx.logger.warn(`evolution-state-json: corrupt ${PENDING_ARCHIVE_FILE} could not be quarantined to .corrupt — skipping this audit append to preserve the recoverable bytes`);
333
- return current;
361
+ if (current !== null) {
362
+ let parsed;
363
+ let unparseable = false;
364
+ try {
365
+ parsed = JSON.parse(current);
366
+ } catch {
367
+ unparseable = true;
334
368
  }
369
+ if (unparseable || !Array.isArray(parsed)) {
370
+ if (!await io().writeText(`${pathOf(PENDING_ARCHIVE_FILE)}.corrupt`, current).then(() => true, () => false)) {
371
+ ctx.logger.warn(`evolution-state-json: corrupt ${PENDING_ARCHIVE_FILE} could not be quarantined to .corrupt — skipping this audit append to preserve the recoverable bytes`);
372
+ landed = false;
373
+ return current;
374
+ }
375
+ } else archive = parsed;
335
376
  }
336
377
  const shaped = archive.filter((entry) => entry !== null && typeof entry === "object");
337
378
  const archiveKeys = /* @__PURE__ */ new Set();
@@ -348,13 +389,15 @@ function apply(ctx, rawConfig = {}) {
348
389
  if (fresh.length === 0 && !hadDuplicates) return current;
349
390
  const next = [...archive, ...fresh];
350
391
  if (next.length > ARCHIVE_RESOLVED_CAP) {
351
- if (archive.length > 0) await io().writeText(pathOf(PENDING_ARCHIVE_BAK_FILE), JSON.stringify(archive, null, 2)).catch(() => {});
352
- return JSON.stringify((fresh.length > 0 ? fresh : next).slice(-5e3), null, 2);
392
+ if (archive.length > 0) await io().writeText(pathOf(PENDING_ARCHIVE_BAK_FILE), JSON.stringify(archive)).catch(() => {});
393
+ return JSON.stringify((fresh.length > 0 ? fresh : next).slice(-5e3));
353
394
  }
354
- return JSON.stringify(next, null, 2);
395
+ return JSON.stringify(next);
355
396
  });
397
+ return landed;
356
398
  } catch (error) {
357
399
  ctx.logger.warn(`evolution-state-json: archive append deferred: ${error instanceof Error ? error.message : String(error)}`);
400
+ return false;
358
401
  }
359
402
  }
360
403
  const provider = {
@@ -468,7 +511,6 @@ function apply(ctx, rawConfig = {}) {
468
511
  record: null,
469
512
  applied: false
470
513
  };
471
- let evicted = [];
472
514
  await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
473
515
  const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
474
516
  const record = map[id] ?? null;
@@ -497,10 +539,18 @@ function apply(ctx, rawConfig = {}) {
497
539
  applied: true
498
540
  };
499
541
  const pruned = enforceResolvedCap(map);
500
- evicted = pruned.evicted;
542
+ if (pruned.evicted.length === 0) return pruned.map;
543
+ if (!await appendArchive(pruned.evicted)) {
544
+ const restored = { ...pruned.map };
545
+ for (const record of pruned.evicted) {
546
+ if (record.id in restored) continue;
547
+ restored[record.id] = record;
548
+ }
549
+ ctx.logger.warn(`evolution-state-json: archive append deferred — ${pruned.evicted.length} evicted resolved record(s) were merged back into the live map (retried on the next resolve)`);
550
+ return restored;
551
+ }
501
552
  return pruned.map;
502
553
  });
503
- if (evicted.length > 0) await appendArchive(evicted);
504
554
  return result;
505
555
  });
506
556
  }
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.3.68",
4
+ "version": "0.3.70",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,18 +31,18 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@deepseek-ai/schemastery": "^3.18.1",
34
- "@lmzhen/dsh-evolution-core": "^0.3.68"
34
+ "@lmzhen/dsh-evolution-core": "^0.3.70"
35
35
  },
36
36
  "peerDependencies": {
37
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
37
+ "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
38
38
  "@deepseek-ai/cordis": "^4.0.1",
39
- "@lmzhen/dsh-evolution-io": "^0.3.68",
40
- "@lmzhen/dsh-evolution-state-storage": "^0.3.68"
39
+ "@lmzhen/dsh-evolution-io": "^0.3.70",
40
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.70"
41
41
  },
42
42
  "devDependencies": {
43
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
44
- "@lmzhen/dsh-evolution-io": "^0.3.68",
45
- "@lmzhen/dsh-evolution-state-storage": "^0.3.68",
46
- "@lmzhen/dsh-evolution-io-node": "^0.3.68"
43
+ "@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
44
+ "@lmzhen/dsh-evolution-io": "^0.3.70",
45
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.70",
46
+ "@lmzhen/dsh-evolution-io-node": "^0.3.70"
47
47
  }
48
48
  }