@lmzhen/dsh-evolution-state-json 0.3.64 → 0.3.66
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 +45 -37
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PROVIDER_JSON, REVIEW_STATE_FILE, canClaimPending, canResolvePending, releasedStatus } from "@lmzhen/dsh-evolution-state-storage";
|
|
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_TABLE, assertCloneable, canClaimPending, canResolvePending, recordIssue, releasedStatus } from "@lmzhen/dsh-evolution-state-storage";
|
|
4
4
|
import { isAbsolute, join } from "node:path";
|
|
5
5
|
//#region lib/types/index.js
|
|
6
6
|
/**
|
|
@@ -56,8 +56,17 @@ const QUARANTINE_ERROR_NAME = "EvolutionStateCorruptFile";
|
|
|
56
56
|
* accumulate. The old `.corrupt-<stamp>-<rand>` name minted a fresh file on
|
|
57
57
|
* EVERY read of a corrupt file: unbounded growth with no sweep. The fixed
|
|
58
58
|
* copy is swept after 7 days by the node backend's sweepStaleTmps (S-10). */
|
|
59
|
+
/** P2-19 (v19): pick the quarantine destination. The documented fixed name is
|
|
60
|
+
* reused when it already holds the SAME bytes (a repeated read of one corrupt
|
|
61
|
+
* file must still yield exactly one copy — V10-05's bounded-growth rule); a
|
|
62
|
+
* DIFFERENT payload gets a stamped sibling so the earlier rescue copy is never
|
|
63
|
+
* overwritten. The node backend's 7-day `.corrupt` sweep bounds the set. */
|
|
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()}`;
|
|
67
|
+
}
|
|
59
68
|
async function quarantine(io, root, file, raw, reason) {
|
|
60
|
-
const dest = `${join(root, file)}.corrupt
|
|
69
|
+
const dest = await quarantineTarget(io, `${join(root, file)}.corrupt`, raw);
|
|
61
70
|
let preservedNote = `; original preserved at ${dest} — inspect and fix it, then retry.`;
|
|
62
71
|
try {
|
|
63
72
|
await io().writeText(dest, raw);
|
|
@@ -76,27 +85,12 @@ function firstNonRecordValue(parsed) {
|
|
|
76
85
|
for (const [recordId, record] of Object.entries(parsed)) if (!isPlainRecord(record)) return `expected a plain object for record "${recordId}", got ${record === null ? "null" : Array.isArray(record) ? "an array" : typeof record}`;
|
|
77
86
|
return null;
|
|
78
87
|
}
|
|
79
|
-
const
|
|
80
|
-
const optionalString = (value) => value === void 0 || typeof value === "string";
|
|
81
|
-
const PENDING_KINDS = new Set([
|
|
82
|
-
"memory",
|
|
83
|
-
"skill",
|
|
84
|
-
"capability"
|
|
85
|
-
]);
|
|
86
|
-
const PENDING_STATUSES = new Set([
|
|
87
|
-
"pending",
|
|
88
|
-
"executing",
|
|
89
|
-
"approved",
|
|
90
|
-
"rejected"
|
|
91
|
-
]);
|
|
92
|
-
const gateReviewRecord = (record) => isNonNegInt(record.turnsSinceMemory) && isNonNegInt(record.turnsSinceSkill) && isNonNegInt(record.lastTurn);
|
|
93
|
-
const gateCuratorRecord = (record) => typeof record.lastRunAt === "number" && Number.isFinite(record.lastRunAt) && record.lastRunAt >= 0 && isNonNegInt(record.runCount) && typeof record.lastSummary === "string" && typeof record.paused === "boolean";
|
|
94
|
-
const gatePendingRecord = (record) => typeof record.id === "string" && typeof record.kind === "string" && PENDING_KINDS.has(record.kind) && typeof record.summary === "string" && "args" in record && typeof record.createdAt === "string" && typeof record.status === "string" && PENDING_STATUSES.has(record.status) && optionalString(record.resolvedAt) && optionalString(record.claimedBy) && optionalString(record.claimedAt) && optionalString(record.origin) && optionalString(record.sessionId);
|
|
88
|
+
const gateFor = (table) => (record) => recordIssue(table, record) === null && assertCloneable(record) === null;
|
|
95
89
|
const RECORD_FIELD_GATES = {
|
|
96
|
-
[REVIEW_STATE_FILE]:
|
|
97
|
-
[CURATOR_STATE_FILE]:
|
|
98
|
-
[PENDING_STATE_FILE]:
|
|
99
|
-
[PENDING_LEGACY_FILE]:
|
|
90
|
+
[REVIEW_STATE_FILE]: gateFor(REVIEW_STATE_TABLE),
|
|
91
|
+
[CURATOR_STATE_FILE]: gateFor(CURATOR_STATE_TABLE),
|
|
92
|
+
[PENDING_STATE_FILE]: gateFor(PENDING_TABLE),
|
|
93
|
+
[PENDING_LEGACY_FILE]: gateFor(PENDING_TABLE)
|
|
100
94
|
};
|
|
101
95
|
/** V11-B1 (P2-23): shared per-record field-gate scan for BOTH paths (readJson
|
|
102
96
|
* + jsonTransact — the transaction baseline used to skip the field gates, so
|
|
@@ -139,19 +133,23 @@ function reportGateViolation(ctx, file, failing) {
|
|
|
139
133
|
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(", ")}`);
|
|
140
134
|
}
|
|
141
135
|
async function ensureCorruptCopy(ctx, io, root, file, bad) {
|
|
142
|
-
const
|
|
136
|
+
const base = `${join(root, file)}.corrupt`;
|
|
143
137
|
const corruptKey = JSON.stringify(Object.entries(bad).map(([id, record]) => ({
|
|
144
138
|
id,
|
|
145
139
|
fields: typeof record === "object" && record !== null ? Object.entries(record).map(([field, value]) => `${field}:${Array.isArray(value) ? "array" : typeof value}`).sort() : [typeof record]
|
|
146
140
|
})).sort((a, b) => a.id.localeCompare(b.id)));
|
|
147
|
-
if (corruptWritten.get(file) === corruptKey && await io().exists(
|
|
148
|
-
|
|
141
|
+
if (corruptWritten.get(file) === corruptKey && await io().exists(base)) return true;
|
|
142
|
+
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);
|
|
145
|
+
if (wrote) {
|
|
149
146
|
corruptWritten.set(file, corruptKey);
|
|
150
147
|
corruptWriteWarned.delete(file);
|
|
151
148
|
} else if (!corruptWriteWarned.has(file)) {
|
|
152
149
|
corruptWriteWarned.add(file);
|
|
153
|
-
ctx.logger.warn(`evolution-state-json: could not write quarantine copy "${
|
|
150
|
+
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`);
|
|
154
151
|
}
|
|
152
|
+
return wrote;
|
|
155
153
|
}
|
|
156
154
|
async function jsonTransact(ctx, io, root, file, task) {
|
|
157
155
|
await transactIo(io(), join(root, file), async (current) => {
|
|
@@ -172,8 +170,8 @@ async function jsonTransact(ctx, io, root, file, task) {
|
|
|
172
170
|
for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
|
|
173
171
|
else good[id] = record;
|
|
174
172
|
reportGateViolation(ctx, file, failing);
|
|
175
|
-
await ensureCorruptCopy(ctx, io, root, file, bad);
|
|
176
|
-
|
|
173
|
+
if (await ensureCorruptCopy(ctx, io, root, file, bad)) parsed = good;
|
|
174
|
+
else 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`);
|
|
177
175
|
}
|
|
178
176
|
}
|
|
179
177
|
const next = await task(parsed);
|
|
@@ -241,14 +239,21 @@ function apply(ctx, rawConfig = {}) {
|
|
|
241
239
|
}
|
|
242
240
|
return retired;
|
|
243
241
|
}
|
|
244
|
-
async function retireLegacyOnce(legacy
|
|
242
|
+
async function retireLegacyOnce(legacy) {
|
|
245
243
|
if (legacyMigrated) return {};
|
|
246
244
|
try {
|
|
247
|
-
const retired =
|
|
248
|
-
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, (fresh) =>
|
|
249
|
-
...
|
|
250
|
-
|
|
251
|
-
|
|
245
|
+
const retired = {};
|
|
246
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (fresh) => {
|
|
247
|
+
const merged = { ...fresh ?? {} };
|
|
248
|
+
const archivedIds = await readArchivedIds();
|
|
249
|
+
for (const [id, record] of Object.entries(legacy)) {
|
|
250
|
+
if (id in merged) continue;
|
|
251
|
+
if (archivedIds.has(id)) continue;
|
|
252
|
+
merged[id] = record;
|
|
253
|
+
retired[id] = record;
|
|
254
|
+
}
|
|
255
|
+
return merged;
|
|
256
|
+
});
|
|
252
257
|
await io().rename(pathOf(PENDING_LEGACY_FILE), `${pathOf(PENDING_LEGACY_FILE)}.migrated`);
|
|
253
258
|
legacyMigrated = true;
|
|
254
259
|
return retired;
|
|
@@ -261,7 +266,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
261
266
|
async function loadPendingMap() {
|
|
262
267
|
const [current, legacy] = await Promise.all([readJson(PENDING_STATE_FILE), readJson(PENDING_LEGACY_FILE)]);
|
|
263
268
|
if (legacy !== null) return {
|
|
264
|
-
...await retireLegacyOnce(legacy
|
|
269
|
+
...await retireLegacyOnce(legacy),
|
|
265
270
|
...current ?? {}
|
|
266
271
|
};
|
|
267
272
|
return { ...current ?? {} };
|
|
@@ -284,7 +289,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
284
289
|
* live work and are never trimmed. Returns the pruned map (rather than
|
|
285
290
|
* mutating in place) plus the evicted records. */
|
|
286
291
|
function enforceResolvedCap(map) {
|
|
287
|
-
const resolved = Object.values(map).filter((record) => record.status === "approved" || record.status === "rejected");
|
|
292
|
+
const resolved = Object.values(map).filter((record) => (record.status === "approved" || record.status === "rejected") && record.kind !== "capability");
|
|
288
293
|
if (resolved.length <= PENDING_RESOLVED_CAP$1) return {
|
|
289
294
|
map,
|
|
290
295
|
evicted: []
|
|
@@ -323,7 +328,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
323
328
|
const parsed = JSON.parse(current);
|
|
324
329
|
if (Array.isArray(parsed)) archive = parsed;
|
|
325
330
|
} catch {
|
|
326
|
-
await io().writeText(`${pathOf(PENDING_ARCHIVE_FILE)}.corrupt`, current).
|
|
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;
|
|
334
|
+
}
|
|
327
335
|
}
|
|
328
336
|
const shaped = archive.filter((entry) => entry !== null && typeof entry === "object");
|
|
329
337
|
const archiveKeys = /* @__PURE__ */ new Set();
|
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.66",
|
|
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.66"
|
|
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.66",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.66"
|
|
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.66",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.66",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.66"
|
|
47
47
|
}
|
|
48
48
|
}
|