@lmzhen/dsh-evolution-state-json 0.3.56 → 0.3.57
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 +48 -25
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -57,8 +57,13 @@ const QUARANTINE_ERROR_NAME = "EvolutionStateCorruptFile";
|
|
|
57
57
|
* copy is swept after 7 days by the node backend's sweepStaleTmps (S-10). */
|
|
58
58
|
async function quarantine(io, root, file, raw, reason) {
|
|
59
59
|
const dest = `${join(root, file)}.corrupt`;
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
let preservedNote = `; original preserved at ${dest} — inspect and fix it, then retry.`;
|
|
61
|
+
try {
|
|
62
|
+
await io().writeText(dest, raw);
|
|
63
|
+
} catch (writeError) {
|
|
64
|
+
preservedNote = `; the quarantine copy at ${dest} FAILED to write (${writeError instanceof Error ? writeError.message : String(writeError)}) — the original file is left in place.`;
|
|
65
|
+
}
|
|
66
|
+
throw Object.assign(/* @__PURE__ */ new Error(`evolution state file "${file}" is not valid JSON (${reason})${preservedNote}`), { name: QUARANTINE_ERROR_NAME });
|
|
62
67
|
}
|
|
63
68
|
/** S-05: the V8-16 record-shape gate existed as two verbatim ~15-line
|
|
64
69
|
* copies (jsonTransact + readJson); this private helper is the single owner.
|
|
@@ -91,6 +96,16 @@ const RECORD_FIELD_GATES = {
|
|
|
91
96
|
[PENDING_STATE_FILE]: gatePendingRecord,
|
|
92
97
|
[PENDING_LEGACY_FILE]: gatePendingRecord
|
|
93
98
|
};
|
|
99
|
+
/** V11-B1 (P2-23): shared per-record field-gate scan for BOTH paths (readJson
|
|
100
|
+
* + jsonTransact — the transaction baseline used to skip the field gates, so
|
|
101
|
+
* a string `runCount` became `"x1"` through `(current?.runCount ?? 0) + 1` at
|
|
102
|
+
* the next save). Returns the failing [id, record] entries; empty when the
|
|
103
|
+
* file has no gate, the value is not a plain map, or everything passes. */
|
|
104
|
+
function gateScan(file, parsed) {
|
|
105
|
+
const gate = RECORD_FIELD_GATES[file];
|
|
106
|
+
if (gate === void 0 || !isPlainRecord(parsed)) return [];
|
|
107
|
+
return Object.entries(parsed).filter(([, record]) => !isPlainRecord(record) || !gate(record));
|
|
108
|
+
}
|
|
94
109
|
/**
|
|
95
110
|
* Cross-process JSON-file RMW (v3-audit M-8): every read-modify-write state
|
|
96
111
|
* mutation runs inside the IO backend's transact lock (via transactIo) so a
|
|
@@ -116,6 +131,15 @@ async function jsonTransact(io, root, file, task) {
|
|
|
116
131
|
if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(io, root, file, current, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
|
|
117
132
|
const malformed = firstNonRecordValue(parsed);
|
|
118
133
|
if (malformed !== null) return await quarantine(io, root, file, current, malformed);
|
|
134
|
+
const failing = gateScan(file, parsed);
|
|
135
|
+
if (failing.length > 0) {
|
|
136
|
+
const bad = {};
|
|
137
|
+
const good = {};
|
|
138
|
+
for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
|
|
139
|
+
else good[id] = record;
|
|
140
|
+
await io().writeText(`${join(root, file)}.corrupt`, JSON.stringify(bad, null, 2)).catch(() => {});
|
|
141
|
+
parsed = good;
|
|
142
|
+
}
|
|
119
143
|
}
|
|
120
144
|
const next = await task(parsed);
|
|
121
145
|
if (next !== null && RECORD_MAP_FILES.has(file) && !isPlainRecord(next)) throw new Error(`evolution state file "${file}" task returned ${Array.isArray(next) ? "an array" : typeof next} (expected null or a plain JSON object map of records); not written.`);
|
|
@@ -128,6 +152,7 @@ function apply(ctx, rawConfig) {
|
|
|
128
152
|
const io = () => ctx.evolutionIo.provider();
|
|
129
153
|
const pathOf = (file) => join(root, file);
|
|
130
154
|
const recordGateWarned = /* @__PURE__ */ new Set();
|
|
155
|
+
const corruptWritten = /* @__PURE__ */ new Map();
|
|
131
156
|
async function readJson(file) {
|
|
132
157
|
const raw = await io().readText(pathOf(file));
|
|
133
158
|
if (raw === null) return null;
|
|
@@ -140,30 +165,28 @@ function apply(ctx, rawConfig) {
|
|
|
140
165
|
if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(io, root, file, raw, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
|
|
141
166
|
const malformed = firstNonRecordValue(parsed);
|
|
142
167
|
if (malformed !== null) return await quarantine(io, root, file, raw, malformed);
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
if (failing.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
else good[id] = record;
|
|
168
|
+
const failing = gateScan(file, parsed);
|
|
169
|
+
if (failing.length > 0) {
|
|
170
|
+
const bad = {};
|
|
171
|
+
const good = {};
|
|
172
|
+
for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
|
|
173
|
+
else good[id] = record;
|
|
174
|
+
const corruptKey = JSON.stringify(failing.map(([id]) => id).sort());
|
|
175
|
+
if (corruptWritten.get(file) !== corruptKey) {
|
|
152
176
|
await io().writeText(`${pathOf(file)}.corrupt`, JSON.stringify(bad, null, 2)).catch(() => {});
|
|
153
|
-
|
|
154
|
-
recordGateWarned.add(file);
|
|
155
|
-
ctx.logger.warn(`evolution-state-json: ${failing.length} record(s) in "${file}" failed the record schema gate and were quarantined to "${file}.corrupt": ${failing.join(", ")}`);
|
|
156
|
-
}
|
|
157
|
-
return good;
|
|
177
|
+
corruptWritten.set(file, corruptKey);
|
|
158
178
|
}
|
|
179
|
+
if (!recordGateWarned.has(file)) {
|
|
180
|
+
recordGateWarned.add(file);
|
|
181
|
+
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(", ")}`);
|
|
182
|
+
}
|
|
183
|
+
return good;
|
|
159
184
|
}
|
|
160
185
|
return parsed;
|
|
161
186
|
}
|
|
162
187
|
const mutate = makeSerialQueue();
|
|
163
188
|
let legacyMigrated = false;
|
|
164
|
-
let archivedIdsCache = null;
|
|
165
189
|
async function readArchivedIds() {
|
|
166
|
-
if (archivedIdsCache !== null) return archivedIdsCache;
|
|
167
190
|
const ids = /* @__PURE__ */ new Set();
|
|
168
191
|
try {
|
|
169
192
|
const rawArchive = await readJson(PENDING_ARCHIVE_FILE);
|
|
@@ -177,7 +200,6 @@ function apply(ctx, rawConfig) {
|
|
|
177
200
|
}
|
|
178
201
|
} catch {}
|
|
179
202
|
} catch {}
|
|
180
|
-
archivedIdsCache = ids;
|
|
181
203
|
return ids;
|
|
182
204
|
}
|
|
183
205
|
function filterLegacy(legacy, current, archivedIds) {
|
|
@@ -203,7 +225,7 @@ function apply(ctx, rawConfig) {
|
|
|
203
225
|
} catch (error) {
|
|
204
226
|
if (error instanceof Error && error.name === QUARANTINE_ERROR_NAME) throw error;
|
|
205
227
|
ctx.logger.warn(`evolution-state-json: legacy pending retirement deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
-
return
|
|
228
|
+
return {};
|
|
207
229
|
}
|
|
208
230
|
}
|
|
209
231
|
async function loadPendingMap() {
|
|
@@ -271,8 +293,9 @@ function apply(ctx, rawConfig) {
|
|
|
271
293
|
const parsed = JSON.parse(current);
|
|
272
294
|
if (Array.isArray(parsed)) archive = parsed;
|
|
273
295
|
} catch {}
|
|
296
|
+
const shaped = archive.filter((entry) => entry !== null && typeof entry === "object");
|
|
274
297
|
const archiveKeys = /* @__PURE__ */ new Set();
|
|
275
|
-
const collapsed =
|
|
298
|
+
const collapsed = shaped.filter((entry) => {
|
|
276
299
|
const key = pendingArchiveKey(entry);
|
|
277
300
|
if (archiveKeys.has(key)) return false;
|
|
278
301
|
archiveKeys.add(key);
|
|
@@ -280,19 +303,19 @@ function apply(ctx, rawConfig) {
|
|
|
280
303
|
});
|
|
281
304
|
const hadDuplicates = collapsed.length !== archive.length;
|
|
282
305
|
archive = collapsed;
|
|
283
|
-
const seen = new Set(
|
|
306
|
+
const seen = new Set(collapsed.map(pendingArchiveKey));
|
|
284
307
|
const fresh = records.filter((record) => !seen.has(pendingArchiveKey(record)));
|
|
285
308
|
if (fresh.length === 0 && !hadDuplicates) return current;
|
|
286
309
|
const next = [...archive, ...fresh];
|
|
287
310
|
if (next.length > ARCHIVE_RESOLVED_CAP) {
|
|
288
311
|
if (archive.length > 0) await io().writeText(pathOf(PENDING_ARCHIVE_BAK_FILE), JSON.stringify(archive, null, 2)).catch(() => {});
|
|
289
|
-
archivedIdsCache = null;
|
|
290
312
|
return JSON.stringify((fresh.length > 0 ? fresh : next).slice(-5e3), null, 2);
|
|
291
313
|
}
|
|
292
|
-
archivedIdsCache = null;
|
|
293
314
|
return JSON.stringify(next, null, 2);
|
|
294
315
|
});
|
|
295
|
-
} catch {
|
|
316
|
+
} catch (error) {
|
|
317
|
+
ctx.logger.warn(`evolution-state-json: archive append deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
|
+
}
|
|
296
319
|
}
|
|
297
320
|
const provider = {
|
|
298
321
|
name: PROVIDER_JSON,
|
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.57",
|
|
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.57"
|
|
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.57",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.57"
|
|
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.57",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.57",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.57"
|
|
47
47
|
}
|
|
48
48
|
}
|