@lmzhen/dsh-evolution-state-json 0.3.58 → 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 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
- async function jsonTransact(io, root, file, task) {
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
- await io().writeText(`${join(root, file)}.corrupt`, JSON.stringify(bad, null, 2)).catch(() => {});
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
- 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
- }
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)) {
@@ -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.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.58"
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.58",
40
- "@lmzhen/dsh-evolution-state-storage": "^0.3.58"
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.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.59",
45
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.59",
46
+ "@lmzhen/dsh-evolution-io-node": "^0.3.59"
47
47
  }
48
48
  }