@lmzhen/dsh-evolution-state-json 0.3.62 → 0.3.64
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 +28 -13
- package/lib/types/index.d.ts +1 -1
- 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 —
|
|
@@ -90,7 +91,7 @@ const PENDING_STATUSES = new Set([
|
|
|
90
91
|
]);
|
|
91
92
|
const gateReviewRecord = (record) => isNonNegInt(record.turnsSinceMemory) && isNonNegInt(record.turnsSinceSkill) && isNonNegInt(record.lastTurn);
|
|
92
93
|
const gateCuratorRecord = (record) => typeof record.lastRunAt === "number" && Number.isFinite(record.lastRunAt) && record.lastRunAt >= 0 && isNonNegInt(record.runCount) && typeof record.lastSummary === "string" && typeof record.paused === "boolean";
|
|
93
|
-
const gatePendingRecord = (record) => typeof record.id === "string" && typeof record.kind === "string" && PENDING_KINDS.has(record.kind) && typeof record.summary === "string" && typeof record.createdAt === "string" && typeof record.status === "string" && PENDING_STATUSES.has(record.status) && optionalString(record.resolvedAt) && optionalString(record.claimedBy) && optionalString(record.claimedAt) && optionalString(record.origin) && optionalString(record.sessionId);
|
|
94
|
+
const gatePendingRecord = (record) => typeof record.id === "string" && typeof record.kind === "string" && PENDING_KINDS.has(record.kind) && typeof record.summary === "string" && "args" in record && typeof record.createdAt === "string" && typeof record.status === "string" && PENDING_STATUSES.has(record.status) && optionalString(record.resolvedAt) && optionalString(record.claimedBy) && optionalString(record.claimedAt) && optionalString(record.origin) && optionalString(record.sessionId);
|
|
94
95
|
const RECORD_FIELD_GATES = {
|
|
95
96
|
[REVIEW_STATE_FILE]: gateReviewRecord,
|
|
96
97
|
[CURATOR_STATE_FILE]: gateCuratorRecord,
|
|
@@ -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,15 +177,15 @@ 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.`);
|
|
168
181
|
if (next !== null && RECORD_MAP_FILES.has(file)) {
|
|
169
182
|
const failing = gateScan(file, next);
|
|
170
|
-
if (failing.length > 0) throw
|
|
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.`);
|
|
171
184
|
}
|
|
172
185
|
return next === null ? null : JSON.stringify(next, null, 2);
|
|
173
186
|
});
|
|
174
187
|
}
|
|
175
|
-
function apply(ctx, rawConfig) {
|
|
188
|
+
function apply(ctx, rawConfig = {}) {
|
|
176
189
|
const root = (rawConfig.root ?? "").trim() || evolutionHome();
|
|
177
190
|
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`);
|
|
178
191
|
const io = () => ctx.evolutionIo.provider();
|
|
@@ -240,7 +253,7 @@ function apply(ctx, rawConfig) {
|
|
|
240
253
|
legacyMigrated = true;
|
|
241
254
|
return retired;
|
|
242
255
|
} catch (error) {
|
|
243
|
-
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;
|
|
244
257
|
ctx.logger.warn(`evolution-state-json: legacy pending retirement deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
245
258
|
return {};
|
|
246
259
|
}
|
|
@@ -272,11 +285,11 @@ function apply(ctx, rawConfig) {
|
|
|
272
285
|
* mutating in place) plus the evicted records. */
|
|
273
286
|
function enforceResolvedCap(map) {
|
|
274
287
|
const resolved = Object.values(map).filter((record) => record.status === "approved" || record.status === "rejected");
|
|
275
|
-
if (resolved.length <= PENDING_RESOLVED_CAP) return {
|
|
288
|
+
if (resolved.length <= PENDING_RESOLVED_CAP$1) return {
|
|
276
289
|
map,
|
|
277
290
|
evicted: []
|
|
278
291
|
};
|
|
279
|
-
const overflow = resolved.length - PENDING_RESOLVED_CAP;
|
|
292
|
+
const overflow = resolved.length - PENDING_RESOLVED_CAP$1;
|
|
280
293
|
const entryTime = (record) => {
|
|
281
294
|
if (!record.resolvedAt) return Number.MAX_SAFE_INTEGER;
|
|
282
295
|
const parsed = Date.parse(record.resolvedAt);
|
|
@@ -309,7 +322,9 @@ function apply(ctx, rawConfig) {
|
|
|
309
322
|
if (current !== null) try {
|
|
310
323
|
const parsed = JSON.parse(current);
|
|
311
324
|
if (Array.isArray(parsed)) archive = parsed;
|
|
312
|
-
} catch {
|
|
325
|
+
} catch {
|
|
326
|
+
await io().writeText(`${pathOf(PENDING_ARCHIVE_FILE)}.corrupt`, current).catch(() => {});
|
|
327
|
+
}
|
|
313
328
|
const shaped = archive.filter((entry) => entry !== null && typeof entry === "object");
|
|
314
329
|
const archiveKeys = /* @__PURE__ */ new Set();
|
|
315
330
|
const collapsed = shaped.filter((entry) => {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export interface Config {
|
|
|
15
15
|
}
|
|
16
16
|
export declare const Config: z<Config>;
|
|
17
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>;
|
|
18
|
-
export declare function apply(ctx: Context, rawConfig
|
|
18
|
+
export declare function apply(ctx: Context, rawConfig?: Config): void;
|
|
19
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.
|
|
4
|
+
"version": "0.3.64",
|
|
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.64"
|
|
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.64",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.64"
|
|
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.64",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.64",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.64"
|
|
47
47
|
}
|
|
48
48
|
}
|