@lmzhen/dsh-evolution-state-json 0.3.68 → 0.3.69
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 +73 -23
- package/package.json +7 -7
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
|
|
66
|
-
|
|
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
|
-
*
|
|
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)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
352
|
-
return JSON.stringify((fresh.length > 0 ? fresh : next).slice(-5e3)
|
|
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
|
|
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
|
|
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.
|
|
4
|
+
"version": "0.3.69",
|
|
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.
|
|
34
|
+
"@lmzhen/dsh-evolution-core": "^0.3.69"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
39
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
40
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.3.
|
|
39
|
+
"@lmzhen/dsh-evolution-io": "^0.3.69",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.69"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
44
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
45
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.3.
|
|
46
|
-
"@lmzhen/dsh-evolution-io-node": "^0.3.
|
|
44
|
+
"@lmzhen/dsh-evolution-io": "^0.3.69",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.69",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.69"
|
|
47
47
|
}
|
|
48
48
|
}
|