@lmzhen/dsh-evolution-state-json 0.3.65 → 0.3.67
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/README.md +1 -0
- package/lib/index.js +49 -18
- 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 } 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
|
-
|
|
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) {
|
|
@@ -239,14 +245,21 @@ function apply(ctx, rawConfig = {}) {
|
|
|
239
245
|
}
|
|
240
246
|
return retired;
|
|
241
247
|
}
|
|
242
|
-
async function retireLegacyOnce(legacy
|
|
248
|
+
async function retireLegacyOnce(legacy) {
|
|
243
249
|
if (legacyMigrated) return {};
|
|
244
250
|
try {
|
|
245
|
-
const retired =
|
|
246
|
-
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, (fresh) =>
|
|
247
|
-
...
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
const retired = {};
|
|
252
|
+
await jsonTransact(ctx, io, root, PENDING_STATE_FILE, async (fresh) => {
|
|
253
|
+
const merged = { ...fresh ?? {} };
|
|
254
|
+
const archivedIds = await readArchivedIds();
|
|
255
|
+
for (const [id, record] of Object.entries(legacy)) {
|
|
256
|
+
if (id in merged) continue;
|
|
257
|
+
if (archivedIds.has(id)) continue;
|
|
258
|
+
merged[id] = record;
|
|
259
|
+
retired[id] = record;
|
|
260
|
+
}
|
|
261
|
+
return merged;
|
|
262
|
+
});
|
|
250
263
|
await io().rename(pathOf(PENDING_LEGACY_FILE), `${pathOf(PENDING_LEGACY_FILE)}.migrated`);
|
|
251
264
|
legacyMigrated = true;
|
|
252
265
|
return retired;
|
|
@@ -259,7 +272,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
259
272
|
async function loadPendingMap() {
|
|
260
273
|
const [current, legacy] = await Promise.all([readJson(PENDING_STATE_FILE), readJson(PENDING_LEGACY_FILE)]);
|
|
261
274
|
if (legacy !== null) return {
|
|
262
|
-
...await retireLegacyOnce(legacy
|
|
275
|
+
...await retireLegacyOnce(legacy),
|
|
263
276
|
...current ?? {}
|
|
264
277
|
};
|
|
265
278
|
return { ...current ?? {} };
|
|
@@ -267,7 +280,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
267
280
|
/** V6-01 (0.3.34): single-sourced legacy merge for the four mutation paths —
|
|
268
281
|
* the SAME exclusion as the retirement read path, so a mutation that runs
|
|
269
282
|
* before any retirement cannot fixate a ghost pending twin in current
|
|
270
|
-
* (the archive id set is
|
|
283
|
+
* (the archive id set is read fresh on every merge — V11-B2; current-wins
|
|
284
|
+
* covers stale misses). */
|
|
271
285
|
async function mergedWithFilteredLegacy(legacy, current) {
|
|
272
286
|
if (legacy === null) return current;
|
|
273
287
|
return {
|
|
@@ -282,7 +296,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
282
296
|
* live work and are never trimmed. Returns the pruned map (rather than
|
|
283
297
|
* mutating in place) plus the evicted records. */
|
|
284
298
|
function enforceResolvedCap(map) {
|
|
285
|
-
const resolved = Object.values(map).filter((record) => record.status === "approved" || record.status === "rejected");
|
|
299
|
+
const resolved = Object.values(map).filter((record) => (record.status === "approved" || record.status === "rejected") && record.kind !== "capability");
|
|
286
300
|
if (resolved.length <= PENDING_RESOLVED_CAP$1) return {
|
|
287
301
|
map,
|
|
288
302
|
evicted: []
|
|
@@ -321,7 +335,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
321
335
|
const parsed = JSON.parse(current);
|
|
322
336
|
if (Array.isArray(parsed)) archive = parsed;
|
|
323
337
|
} catch {
|
|
324
|
-
await io().writeText(`${pathOf(PENDING_ARCHIVE_FILE)}.corrupt`, current).
|
|
338
|
+
if (!await io().writeText(`${pathOf(PENDING_ARCHIVE_FILE)}.corrupt`, current).then(() => true, () => false)) {
|
|
339
|
+
ctx.logger.warn(`evolution-state-json: corrupt ${PENDING_ARCHIVE_FILE} could not be quarantined to .corrupt — skipping this audit append to preserve the recoverable bytes`);
|
|
340
|
+
return current;
|
|
341
|
+
}
|
|
325
342
|
}
|
|
326
343
|
const shaped = archive.filter((entry) => entry !== null && typeof entry === "object");
|
|
327
344
|
const archiveKeys = /* @__PURE__ */ new Set();
|
|
@@ -351,15 +368,29 @@ function apply(ctx, rawConfig = {}) {
|
|
|
351
368
|
name: PROVIDER_JSON,
|
|
352
369
|
async loadReviewState(sessionId) {
|
|
353
370
|
return await mutate(async () => {
|
|
354
|
-
|
|
371
|
+
const row = (await readJson(REVIEW_STATE_FILE))?.[sessionId] ?? null;
|
|
372
|
+
if (row === null) return null;
|
|
373
|
+
const { updatedAt: _stamp, ...record } = row;
|
|
374
|
+
return record;
|
|
355
375
|
});
|
|
356
376
|
},
|
|
357
377
|
async saveReviewState(sessionId, record) {
|
|
358
378
|
await mutate(async () => {
|
|
359
|
-
await jsonTransact(ctx, io, root, REVIEW_STATE_FILE, (current) =>
|
|
360
|
-
...current ?? {}
|
|
361
|
-
[sessionId]
|
|
362
|
-
|
|
379
|
+
await jsonTransact(ctx, io, root, REVIEW_STATE_FILE, (current) => {
|
|
380
|
+
const stamped = { ...current ?? {} };
|
|
381
|
+
stamped[sessionId] = {
|
|
382
|
+
...record,
|
|
383
|
+
updatedAt: Date.now()
|
|
384
|
+
};
|
|
385
|
+
const others = Object.keys(stamped).filter((id) => id !== sessionId);
|
|
386
|
+
if (others.length < REVIEW_STATE_SESSION_CAP) return stamped;
|
|
387
|
+
const stampOf = (id) => stamped[id]?.updatedAt ?? 0;
|
|
388
|
+
others.sort((a, b) => stampOf(a) - stampOf(b));
|
|
389
|
+
const evict = new Set(others.slice(0, others.length - REVIEW_STATE_SESSION_CAP + 1));
|
|
390
|
+
const pruned = {};
|
|
391
|
+
for (const [id, row] of Object.entries(stamped)) if (!evict.has(id)) pruned[id] = row;
|
|
392
|
+
return pruned;
|
|
393
|
+
});
|
|
363
394
|
});
|
|
364
395
|
},
|
|
365
396
|
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.
|
|
4
|
+
"version": "0.3.67",
|
|
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.67"
|
|
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.67",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.67"
|
|
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.67",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.67",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.67"
|
|
47
47
|
}
|
|
48
48
|
}
|