@lmzhen/dsh-evolution-feedback 0.1.0-rc.71 → 0.1.0-rc.72
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 +72 -30
- package/lib/types/index.d.ts +4 -1
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -49,6 +49,9 @@ const EVENT_LOG_ROTATE_AT = 4e3;
|
|
|
49
49
|
/** Archive file prefix: `events-<lastArchivedSeq>.json`. The active file is
|
|
50
50
|
* `events.json` and never matches this glob. */
|
|
51
51
|
const EVENT_ARCHIVE_PREFIX = "events-";
|
|
52
|
+
/** Archive naming is strictly numeric: a user file such as `events-backup.json`
|
|
53
|
+
* under the same directory is neither read into the timeline nor pruned. */
|
|
54
|
+
const EVENT_ARCHIVE_RE = /^events-(\d+)\.json$/;
|
|
52
55
|
function eventsFile(home) {
|
|
53
56
|
return join(home, "evolution", "events.json");
|
|
54
57
|
}
|
|
@@ -76,6 +79,17 @@ function parseEvolutionEvents(raw) {
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
/**
|
|
82
|
+
* List the numeric archives under the log's directory, sorted ascending by
|
|
83
|
+
* their last-archived seq. Single glob predicate for the timeline, the
|
|
84
|
+
* retention pass and the feedback migration check (rc.72 H-3).
|
|
85
|
+
*/
|
|
86
|
+
async function listEventArchives(io, path) {
|
|
87
|
+
const dir = dirname(path);
|
|
88
|
+
return (await io.list(dir)).filter((name) => EVENT_ARCHIVE_RE.test(name)).sort((a, b) => {
|
|
89
|
+
return Number.parseInt(a.slice(7, a.length - 5), 10) - Number.parseInt(b.slice(7, b.length - 5), 10);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
79
93
|
* Append one event under the write lock (rc.68): `seq` = current max + 1
|
|
80
94
|
* computed inside the transact, so two processes appending concurrently never
|
|
81
95
|
* collide. A malformed log is refused (bytes preserved) and the append fails.
|
|
@@ -88,6 +102,11 @@ function parseEvolutionEvents(raw) {
|
|
|
88
102
|
* archive write and active write leaves both copies, which the timeline merge
|
|
89
103
|
* dedupes by seq. An archive-write failure aborts the append (active keeps the
|
|
90
104
|
* full old content — no loss) and the caller's best-effort handling applies.
|
|
105
|
+
*
|
|
106
|
+
* rc.72 G-1: when the ACTIVE is missing/whitespace but archives exist (a
|
|
107
|
+
* deleted active, or B-2 self-heal), seq derivation consults the archive names
|
|
108
|
+
* — the active restarts AFTER the highest archived seq, never at 1, so a new
|
|
109
|
+
* event can never shadow an archived one in the seq-deduped timeline.
|
|
91
110
|
*/
|
|
92
111
|
async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE_AT) {
|
|
93
112
|
let assigned = 0;
|
|
@@ -98,7 +117,8 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
98
117
|
return current;
|
|
99
118
|
}
|
|
100
119
|
const nextEvents = await rotateIfDue(io, path, parseEvolutionEvents(current), rotateAt);
|
|
101
|
-
|
|
120
|
+
let maxSeq = nextEvents.reduce((max, entry) => Math.max(max, entry.seq), 0);
|
|
121
|
+
if (maxSeq === 0) for (const name of await listEventArchives(io, path)) maxSeq = Math.max(maxSeq, Number.parseInt(name.slice(7, name.length - 5), 10));
|
|
102
122
|
const record = {
|
|
103
123
|
...event,
|
|
104
124
|
seq: maxSeq + 1,
|
|
@@ -118,14 +138,16 @@ async function appendEvolutionEvent(io, path, event, rotateAt = EVENT_LOG_ROTATE
|
|
|
118
138
|
* `events-<lastArchivedSeq>.json` (await — a failed archive write aborts the
|
|
119
139
|
* append so the active is never truncated without its copy), old archives are
|
|
120
140
|
* pruned, and the newer half is returned as the next active body. No-op when
|
|
121
|
-
* under the threshold.
|
|
141
|
+
* under the threshold; `rotateAt < 2` is a guarded no-op (rc.72 G-1: a
|
|
142
|
+
* one-event rotate would archive everything and restart seqs at 1).
|
|
122
143
|
*/
|
|
123
144
|
async function rotateIfDue(io, path, events, rotateAt) {
|
|
124
|
-
if (events.length < rotateAt) return events;
|
|
145
|
+
if (rotateAt < 2 || events.length < rotateAt) return events;
|
|
125
146
|
const mid = Math.ceil(events.length / 2);
|
|
126
147
|
const head = events.slice(0, mid);
|
|
127
148
|
const tail = events.slice(mid);
|
|
128
|
-
|
|
149
|
+
if (tail.length === 0) return events;
|
|
150
|
+
const anchor = tail[0]?.seq ?? 0;
|
|
129
151
|
const archivePath = join(dirname(path), `${EVENT_ARCHIVE_PREFIX}${anchor - 1}.json`);
|
|
130
152
|
await io.writeText(archivePath, JSON.stringify({
|
|
131
153
|
version: 1,
|
|
@@ -137,21 +159,28 @@ async function rotateIfDue(io, path, events, rotateAt) {
|
|
|
137
159
|
/**
|
|
138
160
|
* Prune old event archives (rc.71): keep the newest `EVENT_LOG_RETAIN_ARCHIVES`.
|
|
139
161
|
* The name's numeric part is the last archived seq, so ordering is NUMERIC —
|
|
140
|
-
* lexicographic would rank `events-10` before `events-2`.
|
|
141
|
-
*
|
|
162
|
+
* lexicographic would rank `events-10` before `events-2`. Only strictly
|
|
163
|
+
* numeric names participate (rc.72 G-2: user files are never deleted).
|
|
164
|
+
* Best-effort per removal; exported for the retention test.
|
|
142
165
|
*/
|
|
143
166
|
async function retainEventArchives(io, path) {
|
|
144
167
|
const dir = dirname(path);
|
|
145
|
-
const names =
|
|
146
|
-
const archiveSeq = (name) => Number.parseInt(name.slice(7, name.length - 5), 10) || 0;
|
|
147
|
-
names.sort((a, b) => archiveSeq(a) - archiveSeq(b));
|
|
168
|
+
const names = await listEventArchives(io, path);
|
|
148
169
|
const excess = names.slice(0, Math.max(0, names.length - 10));
|
|
149
170
|
for (const name of excess) await io.remove(join(dir, name)).catch(() => {});
|
|
150
171
|
}
|
|
151
172
|
/** Read the event log; a missing/whitespace-only file reads as empty,
|
|
152
173
|
* corrupt content is flagged (and refused on append). */
|
|
153
174
|
async function readEvolutionEvents(io, path) {
|
|
154
|
-
|
|
175
|
+
let raw;
|
|
176
|
+
try {
|
|
177
|
+
raw = await io.readText(path);
|
|
178
|
+
} catch {
|
|
179
|
+
return {
|
|
180
|
+
events: [],
|
|
181
|
+
malformed: true
|
|
182
|
+
};
|
|
183
|
+
}
|
|
155
184
|
if (raw === null || raw.trim() === "") return {
|
|
156
185
|
events: [],
|
|
157
186
|
malformed: false
|
|
@@ -177,14 +206,14 @@ async function readEvolutionEvents(io, path) {
|
|
|
177
206
|
* Read the full timeline (rc.71): active log + all archives, merged by seq
|
|
178
207
|
* (active copy wins, duplicates only arise from the rotation crash window),
|
|
179
208
|
* sorted ascending. Per-file malformed flag as in `readEvolutionEvents`; a
|
|
180
|
-
* malformed ARCHIVE is skipped
|
|
209
|
+
* malformed (or unreadable) ARCHIVE is skipped — it never bricks the boot and
|
|
210
|
+
* it is still flagged.
|
|
181
211
|
*/
|
|
182
212
|
async function readEvolutionTimeline(io, path) {
|
|
183
213
|
const dir = dirname(path);
|
|
184
|
-
const names = (await io.list(dir)).filter((name) => name.startsWith("events-") && name.endsWith(".json")).sort();
|
|
185
214
|
let malformed = false;
|
|
186
215
|
const bySeq = /* @__PURE__ */ new Map();
|
|
187
|
-
for (const name of
|
|
216
|
+
for (const name of await listEventArchives(io, path)) {
|
|
188
217
|
const read = await readEvolutionEvents(io, join(dir, name));
|
|
189
218
|
if (read.malformed) malformed = true;
|
|
190
219
|
for (const event of read.events) bySeq.set(event.seq, event);
|
|
@@ -441,6 +470,12 @@ new RegExp(String.raw`ignore\s+${FILLER}(?:previous|above|prior|all)\s+${FILLER}
|
|
|
441
470
|
* @module @lmzhen/dsh-evolution-feedback
|
|
442
471
|
*/
|
|
443
472
|
const CACHE_VERSION = 2;
|
|
473
|
+
/** Cache snapshot cadence (rc.72 G-3): every N-th appended event refreshes the
|
|
474
|
+
* boot cache, so `cache.lastSeq` always stays inside the retention window —
|
|
475
|
+
* a hard crash between snapshots loses at most N events, all of which still
|
|
476
|
+
* live in the ACTIVE log (bounded by `EVENT_LOG_ROTATE_AT`), so the next fold
|
|
477
|
+
* is complete. Package-private tunable, not a config surface. */
|
|
478
|
+
const CACHE_SNAP_EVERY = 1024;
|
|
444
479
|
var EvolutionFeedback = class {
|
|
445
480
|
state = {
|
|
446
481
|
skills: {},
|
|
@@ -468,7 +503,7 @@ var EvolutionFeedback = class {
|
|
|
468
503
|
if (!path || !eventsPath) return;
|
|
469
504
|
await this.mutate(async () => {
|
|
470
505
|
const rawEvents = await io.readText(eventsPath);
|
|
471
|
-
const archiveNames =
|
|
506
|
+
const archiveNames = await listEventArchives(io, eventsPath);
|
|
472
507
|
if ((rawEvents === null || rawEvents.trim() === "") && archiveNames.length === 0) {
|
|
473
508
|
const aggregate = parseAggregate(await io.readText(path));
|
|
474
509
|
if (aggregate) try {
|
|
@@ -478,7 +513,9 @@ var EvolutionFeedback = class {
|
|
|
478
513
|
const { events } = await readEvolutionTimeline(io, eventsPath);
|
|
479
514
|
const cache = parseCache(await io.readText(path));
|
|
480
515
|
const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
481
|
-
const
|
|
516
|
+
const floor = events[0]?.seq ?? 0;
|
|
517
|
+
const usableCache = cache && cache.lastSeq >= floor - 1 ? cache : null;
|
|
518
|
+
const truth = usableCache ? foldWithDelta(usableCache, events) : foldFeedbackState(events);
|
|
482
519
|
this.state = {
|
|
483
520
|
skills: {
|
|
484
521
|
...truth.skills,
|
|
@@ -513,13 +550,13 @@ var EvolutionFeedback = class {
|
|
|
513
550
|
if (!recordIo || !eventsPath) return;
|
|
514
551
|
this.mutate(async () => {
|
|
515
552
|
try {
|
|
516
|
-
await appendEvolutionEvent(recordIo, eventsPath, {
|
|
553
|
+
if (await appendEvolutionEvent(recordIo, eventsPath, {
|
|
517
554
|
type: "feedback",
|
|
518
555
|
target,
|
|
519
556
|
kind,
|
|
520
557
|
rating,
|
|
521
558
|
note
|
|
522
|
-
});
|
|
559
|
+
}) % CACHE_SNAP_EVERY === 0) await this.writeCacheNow();
|
|
523
560
|
} catch (error) {}
|
|
524
561
|
});
|
|
525
562
|
}
|
|
@@ -540,23 +577,28 @@ var EvolutionFeedback = class {
|
|
|
540
577
|
waitIdle() {
|
|
541
578
|
return this.chain;
|
|
542
579
|
}
|
|
543
|
-
/**
|
|
544
|
-
|
|
580
|
+
/** Snapshot the boot cache from the log truth (rc.68/rc.72); best-effort. */
|
|
581
|
+
async writeCacheNow() {
|
|
545
582
|
const path = this.path;
|
|
546
583
|
const eventsPath = this.eventsPath;
|
|
547
584
|
const recordIo = this.io;
|
|
548
|
-
if (!path || !eventsPath || !recordIo) return
|
|
585
|
+
if (!path || !eventsPath || !recordIo) return;
|
|
586
|
+
try {
|
|
587
|
+
const { events } = await readEvolutionTimeline(recordIo, eventsPath);
|
|
588
|
+
const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
589
|
+
if (maxSeq === 0) return;
|
|
590
|
+
await recordIo.writeText(path, JSON.stringify({
|
|
591
|
+
version: CACHE_VERSION,
|
|
592
|
+
lastSeq: maxSeq,
|
|
593
|
+
...foldFeedbackState(events)
|
|
594
|
+
}, null, 2));
|
|
595
|
+
} catch {}
|
|
596
|
+
}
|
|
597
|
+
/** Rebuild the boot cache from the log truth (rc.68); best-effort, queued
|
|
598
|
+
* on the record chain so it runs after the pending appends. */
|
|
599
|
+
persistCache() {
|
|
549
600
|
return this.mutate(async () => {
|
|
550
|
-
|
|
551
|
-
const { events } = await readEvolutionTimeline(recordIo, eventsPath);
|
|
552
|
-
const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
553
|
-
if (maxSeq === 0) return;
|
|
554
|
-
await recordIo.writeText(path, JSON.stringify({
|
|
555
|
-
version: CACHE_VERSION,
|
|
556
|
-
lastSeq: maxSeq,
|
|
557
|
-
...foldFeedbackState(events)
|
|
558
|
-
}, null, 2));
|
|
559
|
-
} catch {}
|
|
601
|
+
await this.writeCacheNow();
|
|
560
602
|
});
|
|
561
603
|
}
|
|
562
604
|
};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -44,7 +44,10 @@ export declare class EvolutionFeedback {
|
|
|
44
44
|
snapshot(): FeedbackState;
|
|
45
45
|
/** Await the pending record-task chain (unload safety; rc.66). */
|
|
46
46
|
waitIdle(): Promise<unknown>;
|
|
47
|
-
/**
|
|
47
|
+
/** Snapshot the boot cache from the log truth (rc.68/rc.72); best-effort. */
|
|
48
|
+
private writeCacheNow;
|
|
49
|
+
/** Rebuild the boot cache from the log truth (rc.68); best-effort, queued
|
|
50
|
+
* on the record chain so it runs after the pending appends. */
|
|
48
51
|
persistCache(): Promise<void>;
|
|
49
52
|
}
|
|
50
53
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-feedback",
|
|
3
3
|
"description": "Feedback-to-quality scoring for self-evolution (community build)",
|
|
4
|
-
"version": "0.1.0-rc.
|
|
4
|
+
"version": "0.1.0-rc.72",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
|
-
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.
|
|
41
|
-
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.
|
|
40
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.72",
|
|
41
|
+
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.72"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
45
|
-
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.
|
|
46
|
-
"@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.
|
|
47
|
-
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.
|
|
45
|
+
"@lmzhen/dsh-evolution-io": "^0.1.0-rc.72",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.72",
|
|
47
|
+
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.72"
|
|
48
48
|
}
|
|
49
49
|
}
|