@lmzhen/dsh-evolution-state-json 0.3.63 → 0.3.65
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 +30 -30
- package/lib/types/index.d.ts +1 -1
- 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" && 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);
|
|
@@ -185,7 +183,7 @@ async function jsonTransact(ctx, io, root, file, task) {
|
|
|
185
183
|
return next === null ? null : JSON.stringify(next, null, 2);
|
|
186
184
|
});
|
|
187
185
|
}
|
|
188
|
-
function apply(ctx, rawConfig) {
|
|
186
|
+
function apply(ctx, rawConfig = {}) {
|
|
189
187
|
const root = (rawConfig.root ?? "").trim() || evolutionHome();
|
|
190
188
|
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`);
|
|
191
189
|
const io = () => ctx.evolutionIo.provider();
|
|
@@ -322,7 +320,9 @@ function apply(ctx, rawConfig) {
|
|
|
322
320
|
if (current !== null) try {
|
|
323
321
|
const parsed = JSON.parse(current);
|
|
324
322
|
if (Array.isArray(parsed)) archive = parsed;
|
|
325
|
-
} catch {
|
|
323
|
+
} catch {
|
|
324
|
+
await io().writeText(`${pathOf(PENDING_ARCHIVE_FILE)}.corrupt`, current).catch(() => {});
|
|
325
|
+
}
|
|
326
326
|
const shaped = archive.filter((entry) => entry !== null && typeof entry === "object");
|
|
327
327
|
const archiveKeys = /* @__PURE__ */ new Set();
|
|
328
328
|
const collapsed = shaped.filter((entry) => {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export interface Config {
|
|
|
15
15
|
}
|
|
16
16
|
export declare const Config: z<Config>;
|
|
17
17
|
export declare function jsonTransact<T>(ctx: Context, io: () => EvolutionIoLike, root: string, file: string, task: (current: T | null) => T | null | Promise<T | null>): Promise<void>;
|
|
18
|
-
export declare function apply(ctx: Context, rawConfig
|
|
18
|
+
export declare function apply(ctx: Context, rawConfig?: Config): void;
|
|
19
19
|
//# 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.3.
|
|
4
|
+
"version": "0.3.65",
|
|
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.65"
|
|
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.65",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.65"
|
|
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.65",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.65",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.65"
|
|
47
47
|
}
|
|
48
48
|
}
|