@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 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
- async function jsonTransact(io, root, file, task) {
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
- await io().writeText(`${join(root, file)}.corrupt`, JSON.stringify(bad, null, 2)).catch(() => {});
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
- const corruptKey = JSON.stringify(failing.map(([id]) => id).sort());
175
- if (corruptWritten.get(file) !== corruptKey) {
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)) {
@@ -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.58",
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.58"
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.58",
40
- "@lmzhen/dsh-evolution-state-storage": "^0.3.58"
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.58",
45
- "@lmzhen/dsh-evolution-state-storage": "^0.3.58",
46
- "@lmzhen/dsh-evolution-io-node": "^0.3.58"
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
  }