@lmzhen/dsh-evolution-state-json 0.3.67 → 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 +80 -36
- 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
|
-
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 } from "@lmzhen/dsh-evolution-state-storage";
|
|
4
|
-
import { isAbsolute, join } from "node:path";
|
|
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 { 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,24 +314,19 @@ 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. */
|
|
298
324
|
function enforceResolvedCap(map) {
|
|
299
|
-
const
|
|
300
|
-
if (
|
|
325
|
+
const oldest = selectPendingOverflow(Object.values(map), PENDING_RESOLVED_CAP$1);
|
|
326
|
+
if (oldest.length === 0) return {
|
|
301
327
|
map,
|
|
302
328
|
evicted: []
|
|
303
329
|
};
|
|
304
|
-
const overflow = resolved.length - PENDING_RESOLVED_CAP$1;
|
|
305
|
-
const entryTime = (record) => {
|
|
306
|
-
if (!record.resolvedAt) return Number.MAX_SAFE_INTEGER;
|
|
307
|
-
const parsed = Date.parse(record.resolvedAt);
|
|
308
|
-
return Number.isNaN(parsed) ? Number.MAX_SAFE_INTEGER : parsed;
|
|
309
|
-
};
|
|
310
|
-
const oldest = resolved.sort((a, b) => entryTime(a) - entryTime(b)).slice(0, overflow);
|
|
311
330
|
const oldestKeys = /* @__PURE__ */ new Set();
|
|
312
331
|
for (const [key, value] of Object.entries(map)) if (oldest.includes(value)) oldestKeys.add(key);
|
|
313
332
|
const kept = {};
|
|
@@ -326,19 +345,34 @@ function apply(ctx, rawConfig = {}) {
|
|
|
326
345
|
* read-only legacy `pending.json` re-introduces an evicted record on the
|
|
327
346
|
* next resolve) and rotate the sidecar to `.bak` past ARCHIVE_RESOLVED_CAP
|
|
328
347
|
* so neither the file nor the per-append full-array rewrite grows without
|
|
329
|
-
* 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. */
|
|
330
356
|
async function appendArchive(records) {
|
|
357
|
+
let landed = true;
|
|
331
358
|
try {
|
|
332
359
|
await transactIo(io(), pathOf(PENDING_ARCHIVE_FILE), async (current) => {
|
|
333
360
|
let archive = [];
|
|
334
|
-
if (current !== null)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
361
|
+
if (current !== null) {
|
|
362
|
+
let parsed;
|
|
363
|
+
let unparseable = false;
|
|
364
|
+
try {
|
|
365
|
+
parsed = JSON.parse(current);
|
|
366
|
+
} catch {
|
|
367
|
+
unparseable = true;
|
|
341
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;
|
|
342
376
|
}
|
|
343
377
|
const shaped = archive.filter((entry) => entry !== null && typeof entry === "object");
|
|
344
378
|
const archiveKeys = /* @__PURE__ */ new Set();
|
|
@@ -355,13 +389,15 @@ function apply(ctx, rawConfig = {}) {
|
|
|
355
389
|
if (fresh.length === 0 && !hadDuplicates) return current;
|
|
356
390
|
const next = [...archive, ...fresh];
|
|
357
391
|
if (next.length > ARCHIVE_RESOLVED_CAP) {
|
|
358
|
-
if (archive.length > 0) await io().writeText(pathOf(PENDING_ARCHIVE_BAK_FILE), JSON.stringify(archive
|
|
359
|
-
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));
|
|
360
394
|
}
|
|
361
|
-
return JSON.stringify(next
|
|
395
|
+
return JSON.stringify(next);
|
|
362
396
|
});
|
|
397
|
+
return landed;
|
|
363
398
|
} catch (error) {
|
|
364
399
|
ctx.logger.warn(`evolution-state-json: archive append deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
400
|
+
return false;
|
|
365
401
|
}
|
|
366
402
|
}
|
|
367
403
|
const provider = {
|
|
@@ -384,9 +420,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
384
420
|
};
|
|
385
421
|
const others = Object.keys(stamped).filter((id) => id !== sessionId);
|
|
386
422
|
if (others.length < REVIEW_STATE_SESSION_CAP) return stamped;
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
423
|
+
const evict = new Set(selectSessionOverflow(others, {
|
|
424
|
+
keyOf: (id) => id,
|
|
425
|
+
stampOf: (id) => stamped[id]?.updatedAt ?? 0
|
|
426
|
+
}));
|
|
390
427
|
const pruned = {};
|
|
391
428
|
for (const [id, row] of Object.entries(stamped)) if (!evict.has(id)) pruned[id] = row;
|
|
392
429
|
return pruned;
|
|
@@ -474,7 +511,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
474
511
|
record: null,
|
|
475
512
|
applied: false
|
|
476
513
|
};
|
|
477
|
-
let evicted = [];
|
|
478
514
|
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
|
|
479
515
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
480
516
|
const record = map[id] ?? null;
|
|
@@ -503,10 +539,18 @@ function apply(ctx, rawConfig = {}) {
|
|
|
503
539
|
applied: true
|
|
504
540
|
};
|
|
505
541
|
const pruned = enforceResolvedCap(map);
|
|
506
|
-
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
|
+
}
|
|
507
552
|
return pruned.map;
|
|
508
553
|
});
|
|
509
|
-
if (evicted.length > 0) await appendArchive(evicted);
|
|
510
554
|
return result;
|
|
511
555
|
});
|
|
512
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
|
}
|