@lmzhen/dsh-evolution-state-json 0.3.58 → 0.3.60
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 +34 -21
- package/lib/types/index.d.ts +1 -14
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -60,6 +60,7 @@ async function quarantine(io, root, file, raw, reason) {
|
|
|
60
60
|
let preservedNote = `; original preserved at ${dest} — inspect and fix it, then retry.`;
|
|
61
61
|
try {
|
|
62
62
|
await io().writeText(dest, raw);
|
|
63
|
+
corruptWritten.delete(file);
|
|
63
64
|
} catch (writeError) {
|
|
64
65
|
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
|
}
|
|
@@ -119,7 +120,27 @@ function gateScan(file, parsed) {
|
|
|
119
120
|
* @internal Exported only for this package's own tests — not public API
|
|
120
121
|
* surface (audit v10 S-03); other packages must go through the provider seam.
|
|
121
122
|
*/
|
|
122
|
-
|
|
123
|
+
const recordGateWarned = /* @__PURE__ */ new Set();
|
|
124
|
+
const corruptWritten = /* @__PURE__ */ new Map();
|
|
125
|
+
const corruptWriteWarned = /* @__PURE__ */ new Set();
|
|
126
|
+
function reportGateViolation(ctx, file, failing) {
|
|
127
|
+
if (recordGateWarned.has(file)) return;
|
|
128
|
+
recordGateWarned.add(file);
|
|
129
|
+
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(", ")}`);
|
|
130
|
+
}
|
|
131
|
+
async function ensureCorruptCopy(ctx, io, root, file, bad) {
|
|
132
|
+
const corruptPath = `${join(root, file)}.corrupt`;
|
|
133
|
+
const corruptKey = JSON.stringify(Object.keys(bad).sort());
|
|
134
|
+
if (corruptWritten.get(file) === corruptKey && await io().exists(corruptPath)) return;
|
|
135
|
+
if (await io().writeText(corruptPath, JSON.stringify(bad, null, 2)).then(() => true).catch(() => false)) {
|
|
136
|
+
corruptWritten.set(file, corruptKey);
|
|
137
|
+
corruptWriteWarned.delete(file);
|
|
138
|
+
} else if (!corruptWriteWarned.has(file)) {
|
|
139
|
+
corruptWriteWarned.add(file);
|
|
140
|
+
ctx.logger.warn(`evolution-state-json: could not write quarantine copy "${corruptPath}" for ${Object.keys(bad).length} failed record(s) — the main file keeps them; the copy is retried on the next access`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function jsonTransact(ctx, io, root, file, task) {
|
|
123
144
|
await transactIo(io(), join(root, file), async (current) => {
|
|
124
145
|
let parsed = null;
|
|
125
146
|
if (current !== null) {
|
|
@@ -137,7 +158,8 @@ async function jsonTransact(io, root, file, task) {
|
|
|
137
158
|
const good = {};
|
|
138
159
|
for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
|
|
139
160
|
else good[id] = record;
|
|
140
|
-
|
|
161
|
+
reportGateViolation(ctx, file, failing);
|
|
162
|
+
await ensureCorruptCopy(ctx, io, root, file, bad);
|
|
141
163
|
parsed = good;
|
|
142
164
|
}
|
|
143
165
|
}
|
|
@@ -151,8 +173,6 @@ function apply(ctx, rawConfig) {
|
|
|
151
173
|
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`);
|
|
152
174
|
const io = () => ctx.evolutionIo.provider();
|
|
153
175
|
const pathOf = (file) => join(root, file);
|
|
154
|
-
const recordGateWarned = /* @__PURE__ */ new Set();
|
|
155
|
-
const corruptWritten = /* @__PURE__ */ new Map();
|
|
156
176
|
async function readJson(file) {
|
|
157
177
|
const raw = await io().readText(pathOf(file));
|
|
158
178
|
if (raw === null) return null;
|
|
@@ -171,15 +191,8 @@ function apply(ctx, rawConfig) {
|
|
|
171
191
|
const good = {};
|
|
172
192
|
for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
|
|
173
193
|
else good[id] = record;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await io().writeText(`${pathOf(file)}.corrupt`, JSON.stringify(bad, null, 2)).catch(() => {});
|
|
177
|
-
corruptWritten.set(file, corruptKey);
|
|
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
|
-
}
|
|
194
|
+
reportGateViolation(ctx, file, failing);
|
|
195
|
+
await ensureCorruptCopy(ctx, io, root, file, bad);
|
|
183
196
|
return good;
|
|
184
197
|
}
|
|
185
198
|
return parsed;
|
|
@@ -215,7 +228,7 @@ function apply(ctx, rawConfig) {
|
|
|
215
228
|
if (legacyMigrated) return {};
|
|
216
229
|
try {
|
|
217
230
|
const retired = filterLegacy(legacy, current, await readArchivedIds());
|
|
218
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, (fresh) => ({
|
|
231
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, (fresh) => ({
|
|
219
232
|
...retired,
|
|
220
233
|
...fresh ?? {}
|
|
221
234
|
}));
|
|
@@ -326,7 +339,7 @@ function apply(ctx, rawConfig) {
|
|
|
326
339
|
},
|
|
327
340
|
async saveReviewState(sessionId, record) {
|
|
328
341
|
await mutate(async () => {
|
|
329
|
-
await jsonTransact(io, root, REVIEW_STATE_FILE, (current) => ({
|
|
342
|
+
await jsonTransact(ctx, io, root, REVIEW_STATE_FILE, (current) => ({
|
|
330
343
|
...current ?? {},
|
|
331
344
|
[sessionId]: record
|
|
332
345
|
}));
|
|
@@ -339,7 +352,7 @@ function apply(ctx, rawConfig) {
|
|
|
339
352
|
},
|
|
340
353
|
async saveCuratorState(record) {
|
|
341
354
|
await mutate(async () => {
|
|
342
|
-
await jsonTransact(io, root, CURATOR_STATE_FILE, (current) => ({
|
|
355
|
+
await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, (current) => ({
|
|
343
356
|
...current ?? {},
|
|
344
357
|
[CURATOR_STATE_KEY]: record
|
|
345
358
|
}));
|
|
@@ -347,7 +360,7 @@ function apply(ctx, rawConfig) {
|
|
|
347
360
|
},
|
|
348
361
|
async transactCuratorState(task) {
|
|
349
362
|
await mutate(async () => {
|
|
350
|
-
await jsonTransact(io, root, CURATOR_STATE_FILE, (current) => {
|
|
363
|
+
await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, (current) => {
|
|
351
364
|
const next = task(current?.[CURATOR_STATE_KEY] ?? null);
|
|
352
365
|
if (next === null) return current;
|
|
353
366
|
return {
|
|
@@ -365,7 +378,7 @@ function apply(ctx, rawConfig) {
|
|
|
365
378
|
},
|
|
366
379
|
async savePending(record) {
|
|
367
380
|
await mutate(async () => {
|
|
368
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, async (current) => {
|
|
381
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
|
|
369
382
|
return {
|
|
370
383
|
...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}),
|
|
371
384
|
[record.id]: record
|
|
@@ -376,7 +389,7 @@ function apply(ctx, rawConfig) {
|
|
|
376
389
|
async claimPending(id, claimId) {
|
|
377
390
|
return await mutate(async () => {
|
|
378
391
|
const slot = { claimed: null };
|
|
379
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, async (current) => {
|
|
392
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
|
|
380
393
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
381
394
|
const record = map[id] ?? null;
|
|
382
395
|
if (record === null || !canClaimPending(record.status)) return map;
|
|
@@ -395,7 +408,7 @@ function apply(ctx, rawConfig) {
|
|
|
395
408
|
},
|
|
396
409
|
async releasePendingClaim(id, claimId) {
|
|
397
410
|
await mutate(async () => {
|
|
398
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, async (current) => {
|
|
411
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
|
|
399
412
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
400
413
|
const record = map[id];
|
|
401
414
|
if (!record || record.claimedBy !== claimId) return map;
|
|
@@ -414,7 +427,7 @@ function apply(ctx, rawConfig) {
|
|
|
414
427
|
applied: false
|
|
415
428
|
};
|
|
416
429
|
let evicted = [];
|
|
417
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, async (current) => {
|
|
430
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
|
|
418
431
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
419
432
|
const record = map[id] ?? null;
|
|
420
433
|
if (record === null || !canResolvePending(record.status)) {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -14,19 +14,6 @@ export interface Config {
|
|
|
14
14
|
root?: string;
|
|
15
15
|
}
|
|
16
16
|
export declare const Config: z<Config>;
|
|
17
|
-
|
|
18
|
-
* Cross-process JSON-file RMW (v3-audit M-8): every read-modify-write state
|
|
19
|
-
* mutation runs inside the IO backend's transact lock (via transactIo) so a
|
|
20
|
-
* second process sharing DSH_HOME cannot interleave its claim/resolve.
|
|
21
|
-
* `task` returns the next value; null = ENSURE-ABSENT (the file is deleted
|
|
22
|
-
* when it exists — the parity of the transact seam, V6-32). For a record-map
|
|
23
|
-
* file the return must be null or a plain object map of records, and an
|
|
24
|
-
* array/scalar would be persisted as a corrupt map — so it fails loud before
|
|
25
|
-
* any write (0.3.28, V4-08). The legacy `pending.json` merge stays inside the
|
|
26
|
-
* task via `readJson` where relevant.
|
|
27
|
-
* @internal Exported only for this package's own tests — not public API
|
|
28
|
-
* surface (audit v10 S-03); other packages must go through the provider seam.
|
|
29
|
-
*/
|
|
30
|
-
export declare function jsonTransact<T>(io: () => EvolutionIoLike, root: string, file: string, task: (current: T | null) => T | null | Promise<T | null>): Promise<void>;
|
|
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>;
|
|
31
18
|
export declare function apply(ctx: Context, rawConfig: Config): void;
|
|
32
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.60",
|
|
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.60"
|
|
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.60",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.60"
|
|
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.60",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.60",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.60"
|
|
47
47
|
}
|
|
48
48
|
}
|