@lmzhen/dsh-evolution-state-json 0.3.61 → 0.3.63

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/lib/index.js +34 -10
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -22,5 +22,6 @@ Independent of request-prefix construction. This package does not alter the asse
22
22
  ## Known Limitations and Deferred Work
23
23
 
24
24
 
25
+ - P2-4 (v15): the live pending map is BOUNDED — resolved (approved/rejected) records are kept to the most recent `PENDING_RESOLVED_CAP` (200, seam constant in `evolution-state-storage`); the oldest by `resolvedAt` rotate into the `pending-state-archive.json` sidecar (with `.bak` rotation), which is JSON-provider-specific. The DOMAIN provider enforces the same live cap but has no sidecar: past the cap its resolved records are deleted, not archived.
25
26
  - JSON provider serializes writers inside one process AND through the IO backend's cross-process transact lock (an internal transact wrapper — not public API, audit v10 S-03 — wraps every mutation, 0.3.20/0.3.27) — this provider is NOT limited to single-process safety. The caveat below is about the DSH storage-domain providers (`storage-json` documents no cross-process write locking) when the DOMAIN provider is used instead; multi-process deployments should route the evolution domain to a backend with cross-process semantics such as SQLite or remote storage.
26
27
 
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { evolutionHome, makeSerialQueue, transactIo } from "@lmzhen/dsh-evolution-core";
3
- import { CURATOR_STATE_FILE, CURATOR_STATE_KEY, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_STATE_FILE, PROVIDER_JSON, REVIEW_STATE_FILE, canClaimPending, canResolvePending, releasedStatus } from "@lmzhen/dsh-evolution-state-storage";
3
+ import { CURATOR_STATE_FILE, CURATOR_STATE_KEY, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PROVIDER_JSON, REVIEW_STATE_FILE, canClaimPending, canResolvePending, releasedStatus } from "@lmzhen/dsh-evolution-state-storage";
4
4
  import { isAbsolute, join } from "node:path";
5
5
  //#region lib/types/index.js
6
6
  /**
@@ -15,9 +15,10 @@ const inject = ["evolutionStateStorage", "evolutionIo"];
15
15
  const Config = z.object({ root: z.string().default("") });
16
16
  /** 0.3.22 (F-336): resolved (approved/rejected) audit records are capped in
17
17
  * the LIVE pending map so a long-running deployment never grows it without
18
- * bound; the oldest over the cap are archived (made package-private so the
19
- * archive sidecar and the provider enforce one number). */
20
- const PENDING_RESOLVED_CAP = 200;
18
+ * bound; the oldest over the cap are archived. P2-4 (v15): the number lives
19
+ * on the seam (`SEAM_PENDING_RESOLVED_CAP`) so the domain provider enforces
20
+ * the same bound; the archive sidecar below stays json-specific. */
21
+ const PENDING_RESOLVED_CAP$1 = PENDING_RESOLVED_CAP;
21
22
  /** 0.3.27 (V4-01): the audit sidecar (pending-state-archive.json) is bounded
22
23
  * at this many resolved records. Past it the oldest history rotates to a
23
24
  * `.bak` sidecar, so the file — and the full-array rewrite on every append —
@@ -123,6 +124,15 @@ function gateScan(file, parsed) {
123
124
  const recordGateWarned = /* @__PURE__ */ new Set();
124
125
  const corruptWritten = /* @__PURE__ */ new Map();
125
126
  const corruptWriteWarned = /* @__PURE__ */ new Set();
127
+ /** P3 (v15): name carried by the two fail-loud write gates in `jsonTransact`
128
+ * (task-return shape, write-back field gate) so best-effort wrappers —
129
+ * `retireLegacyOnce` is the one that matters — rethrow instead of deferring. */
130
+ const WRITE_GATE_ERROR_NAME = "EvolutionStateWriteGate";
131
+ function writeGateError(message) {
132
+ const error = new Error(message);
133
+ error.name = WRITE_GATE_ERROR_NAME;
134
+ return error;
135
+ }
126
136
  function reportGateViolation(ctx, file, failing) {
127
137
  if (recordGateWarned.has(file)) return;
128
138
  recordGateWarned.add(file);
@@ -130,7 +140,10 @@ function reportGateViolation(ctx, file, failing) {
130
140
  }
131
141
  async function ensureCorruptCopy(ctx, io, root, file, bad) {
132
142
  const corruptPath = `${join(root, file)}.corrupt`;
133
- const corruptKey = JSON.stringify(Object.keys(bad).sort());
143
+ const corruptKey = JSON.stringify(Object.entries(bad).map(([id, record]) => ({
144
+ id,
145
+ fields: typeof record === "object" && record !== null ? Object.entries(record).map(([field, value]) => `${field}:${Array.isArray(value) ? "array" : typeof value}`).sort() : [typeof record]
146
+ })).sort((a, b) => a.id.localeCompare(b.id)));
134
147
  if (corruptWritten.get(file) === corruptKey && await io().exists(corruptPath)) return;
135
148
  if (await io().writeText(corruptPath, JSON.stringify(bad, null, 2)).then(() => true).catch(() => false)) {
136
149
  corruptWritten.set(file, corruptKey);
@@ -164,7 +177,11 @@ async function jsonTransact(ctx, io, root, file, task) {
164
177
  }
165
178
  }
166
179
  const next = await task(parsed);
167
- 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.`);
180
+ if (next !== null && RECORD_MAP_FILES.has(file) && !isPlainRecord(next)) throw writeGateError(`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.`);
181
+ if (next !== null && RECORD_MAP_FILES.has(file)) {
182
+ const failing = gateScan(file, next);
183
+ if (failing.length > 0) throw writeGateError(`evolution state file "${file}" write-back carries ${failing.length} record(s) that fail the field gate (${failing.map(([id]) => id).join(", ")}); not written.`);
184
+ }
168
185
  return next === null ? null : JSON.stringify(next, null, 2);
169
186
  });
170
187
  }
@@ -236,7 +253,7 @@ function apply(ctx, rawConfig) {
236
253
  legacyMigrated = true;
237
254
  return retired;
238
255
  } catch (error) {
239
- if (error instanceof Error && error.name === QUARANTINE_ERROR_NAME) throw error;
256
+ if (error instanceof Error && (error.name === QUARANTINE_ERROR_NAME || error.name === WRITE_GATE_ERROR_NAME)) throw error;
240
257
  ctx.logger.warn(`evolution-state-json: legacy pending retirement deferred: ${error instanceof Error ? error.message : String(error)}`);
241
258
  return {};
242
259
  }
@@ -268,11 +285,11 @@ function apply(ctx, rawConfig) {
268
285
  * mutating in place) plus the evicted records. */
269
286
  function enforceResolvedCap(map) {
270
287
  const resolved = Object.values(map).filter((record) => record.status === "approved" || record.status === "rejected");
271
- if (resolved.length <= PENDING_RESOLVED_CAP) return {
288
+ if (resolved.length <= PENDING_RESOLVED_CAP$1) return {
272
289
  map,
273
290
  evicted: []
274
291
  };
275
- const overflow = resolved.length - PENDING_RESOLVED_CAP;
292
+ const overflow = resolved.length - PENDING_RESOLVED_CAP$1;
276
293
  const entryTime = (record) => {
277
294
  if (!record.resolvedAt) return Number.MAX_SAFE_INTEGER;
278
295
  const parsed = Date.parse(record.resolvedAt);
@@ -420,7 +437,7 @@ function apply(ctx, rawConfig) {
420
437
  });
421
438
  });
422
439
  },
423
- async tryResolvePending(id, status) {
440
+ async tryResolvePending(id, status, expectedClaimId) {
424
441
  return await mutate(async () => {
425
442
  let result = {
426
443
  record: null,
@@ -437,6 +454,13 @@ function apply(ctx, rawConfig) {
437
454
  };
438
455
  return map;
439
456
  }
457
+ if (expectedClaimId !== void 0 && record.claimedBy !== expectedClaimId) {
458
+ result = {
459
+ record,
460
+ applied: false
461
+ };
462
+ return map;
463
+ }
440
464
  const resolved = {
441
465
  ...record,
442
466
  status,
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.61",
4
+ "version": "0.3.63",
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.61"
34
+ "@lmzhen/dsh-evolution-core": "^0.3.63"
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.61",
40
- "@lmzhen/dsh-evolution-state-storage": "^0.3.61"
39
+ "@lmzhen/dsh-evolution-io": "^0.3.63",
40
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.63"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
44
- "@lmzhen/dsh-evolution-io": "^0.3.61",
45
- "@lmzhen/dsh-evolution-state-storage": "^0.3.61",
46
- "@lmzhen/dsh-evolution-io-node": "^0.3.61"
44
+ "@lmzhen/dsh-evolution-io": "^0.3.63",
45
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.63",
46
+ "@lmzhen/dsh-evolution-io-node": "^0.3.63"
47
47
  }
48
48
  }