@lmzhen/dsh-evolution-state-json 0.3.57 → 0.3.59
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 +33 -21
- package/lib/types/index.d.ts +1 -14
- package/package.json +7 -7
package/lib/index.js
CHANGED
|
@@ -119,7 +119,27 @@ function gateScan(file, parsed) {
|
|
|
119
119
|
* @internal Exported only for this package's own tests — not public API
|
|
120
120
|
* surface (audit v10 S-03); other packages must go through the provider seam.
|
|
121
121
|
*/
|
|
122
|
-
|
|
122
|
+
const recordGateWarned = /* @__PURE__ */ new Set();
|
|
123
|
+
const corruptWritten = /* @__PURE__ */ new Map();
|
|
124
|
+
const corruptWriteWarned = /* @__PURE__ */ new Set();
|
|
125
|
+
function reportGateViolation(ctx, file, failing) {
|
|
126
|
+
if (recordGateWarned.has(file)) return;
|
|
127
|
+
recordGateWarned.add(file);
|
|
128
|
+
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(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
async function ensureCorruptCopy(ctx, io, root, file, bad) {
|
|
131
|
+
const corruptPath = `${join(root, file)}.corrupt`;
|
|
132
|
+
const corruptKey = JSON.stringify(Object.keys(bad).sort());
|
|
133
|
+
if (corruptWritten.get(file) === corruptKey && await io().exists(corruptPath)) return;
|
|
134
|
+
if (await io().writeText(corruptPath, JSON.stringify(bad, null, 2)).then(() => true).catch(() => false)) {
|
|
135
|
+
corruptWritten.set(file, corruptKey);
|
|
136
|
+
corruptWriteWarned.delete(file);
|
|
137
|
+
} else if (!corruptWriteWarned.has(file)) {
|
|
138
|
+
corruptWriteWarned.add(file);
|
|
139
|
+
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`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function jsonTransact(ctx, io, root, file, task) {
|
|
123
143
|
await transactIo(io(), join(root, file), async (current) => {
|
|
124
144
|
let parsed = null;
|
|
125
145
|
if (current !== null) {
|
|
@@ -137,7 +157,8 @@ async function jsonTransact(io, root, file, task) {
|
|
|
137
157
|
const good = {};
|
|
138
158
|
for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
|
|
139
159
|
else good[id] = record;
|
|
140
|
-
|
|
160
|
+
reportGateViolation(ctx, file, failing);
|
|
161
|
+
await ensureCorruptCopy(ctx, io, root, file, bad);
|
|
141
162
|
parsed = good;
|
|
142
163
|
}
|
|
143
164
|
}
|
|
@@ -151,8 +172,6 @@ function apply(ctx, rawConfig) {
|
|
|
151
172
|
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
173
|
const io = () => ctx.evolutionIo.provider();
|
|
153
174
|
const pathOf = (file) => join(root, file);
|
|
154
|
-
const recordGateWarned = /* @__PURE__ */ new Set();
|
|
155
|
-
const corruptWritten = /* @__PURE__ */ new Map();
|
|
156
175
|
async function readJson(file) {
|
|
157
176
|
const raw = await io().readText(pathOf(file));
|
|
158
177
|
if (raw === null) return null;
|
|
@@ -171,15 +190,8 @@ function apply(ctx, rawConfig) {
|
|
|
171
190
|
const good = {};
|
|
172
191
|
for (const [id, record] of Object.entries(parsed)) if (failing.some(([failedId]) => failedId === id)) bad[id] = record;
|
|
173
192
|
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
|
-
}
|
|
193
|
+
reportGateViolation(ctx, file, failing);
|
|
194
|
+
await ensureCorruptCopy(ctx, io, root, file, bad);
|
|
183
195
|
return good;
|
|
184
196
|
}
|
|
185
197
|
return parsed;
|
|
@@ -215,7 +227,7 @@ function apply(ctx, rawConfig) {
|
|
|
215
227
|
if (legacyMigrated) return {};
|
|
216
228
|
try {
|
|
217
229
|
const retired = filterLegacy(legacy, current, await readArchivedIds());
|
|
218
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, (fresh) => ({
|
|
230
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, (fresh) => ({
|
|
219
231
|
...retired,
|
|
220
232
|
...fresh ?? {}
|
|
221
233
|
}));
|
|
@@ -326,7 +338,7 @@ function apply(ctx, rawConfig) {
|
|
|
326
338
|
},
|
|
327
339
|
async saveReviewState(sessionId, record) {
|
|
328
340
|
await mutate(async () => {
|
|
329
|
-
await jsonTransact(io, root, REVIEW_STATE_FILE, (current) => ({
|
|
341
|
+
await jsonTransact(ctx, io, root, REVIEW_STATE_FILE, (current) => ({
|
|
330
342
|
...current ?? {},
|
|
331
343
|
[sessionId]: record
|
|
332
344
|
}));
|
|
@@ -339,7 +351,7 @@ function apply(ctx, rawConfig) {
|
|
|
339
351
|
},
|
|
340
352
|
async saveCuratorState(record) {
|
|
341
353
|
await mutate(async () => {
|
|
342
|
-
await jsonTransact(io, root, CURATOR_STATE_FILE, (current) => ({
|
|
354
|
+
await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, (current) => ({
|
|
343
355
|
...current ?? {},
|
|
344
356
|
[CURATOR_STATE_KEY]: record
|
|
345
357
|
}));
|
|
@@ -347,7 +359,7 @@ function apply(ctx, rawConfig) {
|
|
|
347
359
|
},
|
|
348
360
|
async transactCuratorState(task) {
|
|
349
361
|
await mutate(async () => {
|
|
350
|
-
await jsonTransact(io, root, CURATOR_STATE_FILE, (current) => {
|
|
362
|
+
await jsonTransact(ctx, io, root, CURATOR_STATE_FILE, (current) => {
|
|
351
363
|
const next = task(current?.[CURATOR_STATE_KEY] ?? null);
|
|
352
364
|
if (next === null) return current;
|
|
353
365
|
return {
|
|
@@ -365,7 +377,7 @@ function apply(ctx, rawConfig) {
|
|
|
365
377
|
},
|
|
366
378
|
async savePending(record) {
|
|
367
379
|
await mutate(async () => {
|
|
368
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, async (current) => {
|
|
380
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
|
|
369
381
|
return {
|
|
370
382
|
...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}),
|
|
371
383
|
[record.id]: record
|
|
@@ -376,7 +388,7 @@ function apply(ctx, rawConfig) {
|
|
|
376
388
|
async claimPending(id, claimId) {
|
|
377
389
|
return await mutate(async () => {
|
|
378
390
|
const slot = { claimed: null };
|
|
379
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, async (current) => {
|
|
391
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
|
|
380
392
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
381
393
|
const record = map[id] ?? null;
|
|
382
394
|
if (record === null || !canClaimPending(record.status)) return map;
|
|
@@ -395,7 +407,7 @@ function apply(ctx, rawConfig) {
|
|
|
395
407
|
},
|
|
396
408
|
async releasePendingClaim(id, claimId) {
|
|
397
409
|
await mutate(async () => {
|
|
398
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, async (current) => {
|
|
410
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
|
|
399
411
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
400
412
|
const record = map[id];
|
|
401
413
|
if (!record || record.claimedBy !== claimId) return map;
|
|
@@ -414,7 +426,7 @@ function apply(ctx, rawConfig) {
|
|
|
414
426
|
applied: false
|
|
415
427
|
};
|
|
416
428
|
let evicted = [];
|
|
417
|
-
await jsonTransact(io, root, PENDING_STATE_FILE, async (current) => {
|
|
429
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (current) => {
|
|
418
430
|
const map = { ...await mergedWithFilteredLegacy(legacyMigrated ? null : await readJson(PENDING_LEGACY_FILE), current ?? {}) };
|
|
419
431
|
const record = map[id] ?? null;
|
|
420
432
|
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.59",
|
|
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.59"
|
|
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.59",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.59"
|
|
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.59",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.59",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.59"
|
|
47
47
|
}
|
|
48
48
|
}
|