@lmzhen/dsh-evolution-state-json 0.3.21 → 0.3.22
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/lib/index.js +68 -6
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -13,6 +13,23 @@ import { join } from "node:path";
|
|
|
13
13
|
const name = "evolution-state-json";
|
|
14
14
|
const inject = ["evolutionStateStorage", "evolutionIo"];
|
|
15
15
|
const Config = z.object({ root: z.string().default("") });
|
|
16
|
+
/** 0.3.22 (F-336): resolved (approved/rejected) audit records are capped in
|
|
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;
|
|
21
|
+
/** 0.3.22 (F-215): a record-map state file must parse to a non-null plain
|
|
22
|
+
* object (a map of records) — valid JSON that is `null`/array/scalar is a
|
|
23
|
+
* corrupt map that used to read as "empty" and was silently overwritten by
|
|
24
|
+
* the next save. Only these four files are record maps; the archive sidecar
|
|
25
|
+
* is a top-level ARRAY and must NOT be gated by this predicate. */
|
|
26
|
+
const isPlainRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
27
|
+
const RECORD_MAP_FILES = new Set([
|
|
28
|
+
"review-state.json",
|
|
29
|
+
"curator-state.json",
|
|
30
|
+
"pending-state.json",
|
|
31
|
+
"pending.json"
|
|
32
|
+
]);
|
|
16
33
|
function apply(ctx, rawConfig) {
|
|
17
34
|
const root = rawConfig.root || evolutionHome();
|
|
18
35
|
const io = () => ctx.evolutionIo.provider();
|
|
@@ -32,7 +49,9 @@ function apply(ctx, rawConfig) {
|
|
|
32
49
|
const raw = await io().readText(pathOf(file));
|
|
33
50
|
if (raw === null) return null;
|
|
34
51
|
try {
|
|
35
|
-
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(file, raw, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
|
|
54
|
+
return parsed;
|
|
36
55
|
} catch (error) {
|
|
37
56
|
return await quarantine(file, raw, error instanceof Error ? error.message : String(error));
|
|
38
57
|
}
|
|
@@ -49,6 +68,7 @@ function apply(ctx, rawConfig) {
|
|
|
49
68
|
let parsed = null;
|
|
50
69
|
if (current !== null) try {
|
|
51
70
|
parsed = JSON.parse(current);
|
|
71
|
+
if (RECORD_MAP_FILES.has(file) && !isPlainRecord(parsed)) return await quarantine(file, current, `expected a plain JSON object (map of records), got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`);
|
|
52
72
|
} catch (error) {
|
|
53
73
|
return await quarantine(file, current, error instanceof Error ? error.message : String(error));
|
|
54
74
|
}
|
|
@@ -64,6 +84,47 @@ function apply(ctx, rawConfig) {
|
|
|
64
84
|
...current ?? {}
|
|
65
85
|
};
|
|
66
86
|
}
|
|
87
|
+
/** 0.3.22 (F-336): when the live pending map holds more than
|
|
88
|
+
* `PENDING_RESOLVED_CAP` resolved records, drop the oldest (by resolvedAt,
|
|
89
|
+
* then insertion order on ties) from the map and return them for archiving.
|
|
90
|
+
* Only approved/rejected records are candidates — pending/executing are
|
|
91
|
+
* live work and are never trimmed. Returns the pruned map (rather than
|
|
92
|
+
* mutating in place) plus the evicted records. */
|
|
93
|
+
function enforceResolvedCap(map) {
|
|
94
|
+
const resolved = Object.values(map).filter((record) => record.status === "approved" || record.status === "rejected");
|
|
95
|
+
if (resolved.length <= PENDING_RESOLVED_CAP) return {
|
|
96
|
+
map,
|
|
97
|
+
evicted: []
|
|
98
|
+
};
|
|
99
|
+
const overflow = resolved.length - PENDING_RESOLVED_CAP;
|
|
100
|
+
const oldest = resolved.sort((a, b) => {
|
|
101
|
+
return (a.resolvedAt ? Date.parse(a.resolvedAt) : Number.MAX_SAFE_INTEGER) - (b.resolvedAt ? Date.parse(b.resolvedAt) : Number.MAX_SAFE_INTEGER);
|
|
102
|
+
}).slice(0, overflow);
|
|
103
|
+
const evictIds = new Set(oldest.map((record) => record.id));
|
|
104
|
+
const kept = {};
|
|
105
|
+
for (const [key, value] of Object.entries(map)) if (!evictIds.has(key)) kept[key] = value;
|
|
106
|
+
return {
|
|
107
|
+
map: kept,
|
|
108
|
+
evicted: oldest
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** 0.3.22 (F-336): append evicted resolved records to an audit sidecar
|
|
112
|
+
* (top-level array, oldest-first). This is a best-effort audit aid: a
|
|
113
|
+
* corrupt/unreadable archive is skipped and an archive write failure must
|
|
114
|
+
* NEVER fail the resolve that triggered it — the live map is already
|
|
115
|
+
* trimmed, so the audit copy is allowed to fall behind. */
|
|
116
|
+
async function appendArchive(records) {
|
|
117
|
+
try {
|
|
118
|
+
await transactIo(io(), pathOf("pending-state-archive.json"), (current) => {
|
|
119
|
+
let archive = [];
|
|
120
|
+
if (current !== null) try {
|
|
121
|
+
const parsed = JSON.parse(current);
|
|
122
|
+
if (Array.isArray(parsed)) archive = parsed;
|
|
123
|
+
} catch {}
|
|
124
|
+
return JSON.stringify([...archive, ...records], null, 2);
|
|
125
|
+
});
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
67
128
|
const provider = {
|
|
68
129
|
name: "json",
|
|
69
130
|
async loadReviewState(sessionId) {
|
|
@@ -96,10 +157,7 @@ function apply(ctx, rawConfig) {
|
|
|
96
157
|
await mutate(async () => {
|
|
97
158
|
await jsonTransact("curator-state.json", (current) => {
|
|
98
159
|
const next = task(current?.primary ?? null);
|
|
99
|
-
if (next === null)
|
|
100
|
-
if (current !== null) delete current.primary;
|
|
101
|
-
return current ?? {};
|
|
102
|
-
}
|
|
160
|
+
if (next === null) return current;
|
|
103
161
|
return {
|
|
104
162
|
...current ?? {},
|
|
105
163
|
primary: next
|
|
@@ -171,6 +229,7 @@ function apply(ctx, rawConfig) {
|
|
|
171
229
|
record: null,
|
|
172
230
|
applied: false
|
|
173
231
|
};
|
|
232
|
+
let evicted = [];
|
|
174
233
|
await jsonTransact("pending-state.json", async (current) => {
|
|
175
234
|
const map = {
|
|
176
235
|
...await readJson("pending.json") ?? {},
|
|
@@ -194,8 +253,11 @@ function apply(ctx, rawConfig) {
|
|
|
194
253
|
record: resolved,
|
|
195
254
|
applied: true
|
|
196
255
|
};
|
|
197
|
-
|
|
256
|
+
const pruned = enforceResolvedCap(map);
|
|
257
|
+
evicted = pruned.evicted;
|
|
258
|
+
return pruned.map;
|
|
198
259
|
});
|
|
260
|
+
if (evicted.length > 0) await appendArchive(evicted);
|
|
199
261
|
return result;
|
|
200
262
|
});
|
|
201
263
|
}
|
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.22",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -31,17 +31,17 @@
|
|
|
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.22"
|
|
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.22",
|
|
40
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.22"
|
|
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.
|
|
44
|
+
"@lmzhen/dsh-evolution-io": "^0.3.22",
|
|
45
|
+
"@lmzhen/dsh-evolution-state-storage": "^0.3.22"
|
|
46
46
|
}
|
|
47
47
|
}
|