@lmzhen/dsh-evolution-state-json 0.3.66 → 0.3.68

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 +33 -18
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -23,5 +23,6 @@ Independent of request-prefix construction. This package does not alter the asse
23
23
 
24
24
 
25
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.
26
+ - V25-11 (v25): the review-state table is likewise BOUNDED — `REVIEW_STATE_SESSION_CAP` (500, seam constant) rows keyed by session; on every save the least-recently-active sessions (provider-stamped `updatedAt`, stored on disk only) are pruned. The stamp is stripped on read, so the consumer-facing record shape is unchanged.
26
27
  - 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.
27
28
 
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, CURATOR_STATE_TABLE, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_TABLE, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_TABLE, assertCloneable, canClaimPending, canResolvePending, recordIssue, releasedStatus } from "@lmzhen/dsh-evolution-state-storage";
3
+ import { CURATOR_STATE_FILE, CURATOR_STATE_KEY, CURATOR_STATE_TABLE, PENDING_ARCHIVE_BAK_FILE, PENDING_ARCHIVE_FILE, PENDING_LEGACY_FILE, PENDING_RESOLVED_CAP, PENDING_STATE_FILE, PENDING_TABLE, PROVIDER_JSON, REVIEW_STATE_FILE, REVIEW_STATE_SESSION_CAP, REVIEW_STATE_TABLE, assertCloneable, canClaimPending, canResolvePending, recordIssue, releasedStatus, selectPendingOverflow, selectSessionOverflow } from "@lmzhen/dsh-evolution-state-storage";
4
4
  import { isAbsolute, join } from "node:path";
5
5
  //#region lib/types/index.js
6
6
  /**
@@ -226,8 +226,14 @@ function apply(ctx, rawConfig = {}) {
226
226
  if (Array.isArray(rawBak)) {
227
227
  for (const entry of rawBak) if (entry && typeof entry.id === "string") ids.add(entry.id);
228
228
  }
229
- } catch {}
230
- } catch {}
229
+ } catch (bakError) {
230
+ if (bakError?.name === QUARANTINE_ERROR_NAME) throw bakError;
231
+ ctx.logger.warn(`evolution-state-json: pending archive .bak unreadable (${bakError instanceof Error ? bakError.message : String(bakError)}) — archived ids that live only in the .bak do not exclude their legacy twins until the sidecar is readable again`);
232
+ }
233
+ } catch (error) {
234
+ if (error?.name === QUARANTINE_ERROR_NAME) throw error;
235
+ ctx.logger.warn(`evolution-state-json: pending archive sidecars unreadable (${error instanceof Error ? error.message : String(error)}) — the V5-02 legacy ghost-twin filter runs WITHOUT the archived-id exclusion until the archive is readable again`);
236
+ }
231
237
  return ids;
232
238
  }
233
239
  function filterLegacy(legacy, current, archivedIds) {
@@ -274,7 +280,8 @@ function apply(ctx, rawConfig = {}) {
274
280
  /** V6-01 (0.3.34): single-sourced legacy merge for the four mutation paths —
275
281
  * the SAME exclusion as the retirement read path, so a mutation that runs
276
282
  * before any retirement cannot fixate a ghost pending twin in current
277
- * (the archive id set is cached once; current-wins covers stale misses). */
283
+ * (the archive id set is read fresh on every merge — V11-B2; current-wins
284
+ * covers stale misses). */
278
285
  async function mergedWithFilteredLegacy(legacy, current) {
279
286
  if (legacy === null) return current;
280
287
  return {
@@ -289,18 +296,11 @@ function apply(ctx, rawConfig = {}) {
289
296
  * live work and are never trimmed. Returns the pruned map (rather than
290
297
  * mutating in place) plus the evicted records. */
291
298
  function enforceResolvedCap(map) {
292
- const resolved = Object.values(map).filter((record) => (record.status === "approved" || record.status === "rejected") && record.kind !== "capability");
293
- if (resolved.length <= PENDING_RESOLVED_CAP$1) return {
299
+ const oldest = selectPendingOverflow(Object.values(map), PENDING_RESOLVED_CAP$1);
300
+ if (oldest.length === 0) return {
294
301
  map,
295
302
  evicted: []
296
303
  };
297
- const overflow = resolved.length - PENDING_RESOLVED_CAP$1;
298
- const entryTime = (record) => {
299
- if (!record.resolvedAt) return Number.MAX_SAFE_INTEGER;
300
- const parsed = Date.parse(record.resolvedAt);
301
- return Number.isNaN(parsed) ? Number.MAX_SAFE_INTEGER : parsed;
302
- };
303
- const oldest = resolved.sort((a, b) => entryTime(a) - entryTime(b)).slice(0, overflow);
304
304
  const oldestKeys = /* @__PURE__ */ new Set();
305
305
  for (const [key, value] of Object.entries(map)) if (oldest.includes(value)) oldestKeys.add(key);
306
306
  const kept = {};
@@ -361,15 +361,30 @@ function apply(ctx, rawConfig = {}) {
361
361
  name: PROVIDER_JSON,
362
362
  async loadReviewState(sessionId) {
363
363
  return await mutate(async () => {
364
- return (await readJson(REVIEW_STATE_FILE))?.[sessionId] ?? null;
364
+ const row = (await readJson(REVIEW_STATE_FILE))?.[sessionId] ?? null;
365
+ if (row === null) return null;
366
+ const { updatedAt: _stamp, ...record } = row;
367
+ return record;
365
368
  });
366
369
  },
367
370
  async saveReviewState(sessionId, record) {
368
371
  await mutate(async () => {
369
- await jsonTransact(ctx, io, root, REVIEW_STATE_FILE, (current) => ({
370
- ...current ?? {},
371
- [sessionId]: record
372
- }));
372
+ await jsonTransact(ctx, io, root, REVIEW_STATE_FILE, (current) => {
373
+ const stamped = { ...current ?? {} };
374
+ stamped[sessionId] = {
375
+ ...record,
376
+ updatedAt: Date.now()
377
+ };
378
+ const others = Object.keys(stamped).filter((id) => id !== sessionId);
379
+ if (others.length < REVIEW_STATE_SESSION_CAP) return stamped;
380
+ const evict = new Set(selectSessionOverflow(others, {
381
+ keyOf: (id) => id,
382
+ stampOf: (id) => stamped[id]?.updatedAt ?? 0
383
+ }));
384
+ const pruned = {};
385
+ for (const [id, row] of Object.entries(stamped)) if (!evict.has(id)) pruned[id] = row;
386
+ return pruned;
387
+ });
373
388
  });
374
389
  },
375
390
  async loadCuratorState() {
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.66",
4
+ "version": "0.3.68",
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.66"
34
+ "@lmzhen/dsh-evolution-core": "^0.3.68"
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.66",
40
- "@lmzhen/dsh-evolution-state-storage": "^0.3.66"
39
+ "@lmzhen/dsh-evolution-io": "^0.3.68",
40
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.68"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
44
- "@lmzhen/dsh-evolution-io": "^0.3.66",
45
- "@lmzhen/dsh-evolution-state-storage": "^0.3.66",
46
- "@lmzhen/dsh-evolution-io-node": "^0.3.66"
44
+ "@lmzhen/dsh-evolution-io": "^0.3.68",
45
+ "@lmzhen/dsh-evolution-state-storage": "^0.3.68",
46
+ "@lmzhen/dsh-evolution-io-node": "^0.3.68"
47
47
  }
48
48
  }