@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.
- package/README.md +1 -0
- package/lib/index.js +34 -10
- 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 (
|
|
19
|
-
*
|
|
20
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
40
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.3.
|
|
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.
|
|
45
|
-
"@lmzhen/dsh-evolution-state-storage": "^0.3.
|
|
46
|
-
"@lmzhen/dsh-evolution-io-node": "^0.3.
|
|
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
|
}
|