@lmzhen/dsh-evolution-feedback 0.1.0-rc.67 → 0.1.0-rc.68
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 +241 -50
- package/lib/types/index.d.ts +12 -2
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -41,6 +41,76 @@ function evolutionIoAdapter(provider) {
|
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
function eventsFile(home) {
|
|
45
|
+
return join(home, "evolution", "events.json");
|
|
46
|
+
}
|
|
47
|
+
function isEventRecord(event) {
|
|
48
|
+
return typeof event === "object" && event !== null && typeof event.seq === "number";
|
|
49
|
+
}
|
|
50
|
+
function parseEventList(raw) {
|
|
51
|
+
if (raw === null) return [];
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(raw);
|
|
54
|
+
if (!Array.isArray(parsed.events)) return [];
|
|
55
|
+
return parsed.events.filter(isEventRecord);
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Append one event under the write lock (rc.68): `seq` = current max + 1
|
|
62
|
+
* computed inside the transact, so two processes appending concurrently never
|
|
63
|
+
* collide. A malformed log is refused (bytes preserved) and the append fails.
|
|
64
|
+
* Returns the assigned seq.
|
|
65
|
+
*/
|
|
66
|
+
async function appendEvolutionEvent(io, path, event) {
|
|
67
|
+
let assigned = 0;
|
|
68
|
+
await transactIo(io, path, (current) => {
|
|
69
|
+
if (current !== null) try {
|
|
70
|
+
JSON.parse(current);
|
|
71
|
+
} catch {
|
|
72
|
+
return Promise.resolve(current);
|
|
73
|
+
}
|
|
74
|
+
const events = parseEventList(current);
|
|
75
|
+
const maxSeq = events.reduce((max, entry) => Math.max(max, entry.seq), 0);
|
|
76
|
+
const record = {
|
|
77
|
+
...event,
|
|
78
|
+
seq: maxSeq + 1,
|
|
79
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
80
|
+
};
|
|
81
|
+
assigned = record.seq;
|
|
82
|
+
return Promise.resolve(JSON.stringify({
|
|
83
|
+
version: 1,
|
|
84
|
+
events: [...events, record]
|
|
85
|
+
}, null, 2));
|
|
86
|
+
});
|
|
87
|
+
if (assigned === 0) throw new Error(`evolution event log is malformed and was not touched: ${path}`);
|
|
88
|
+
return assigned;
|
|
89
|
+
}
|
|
90
|
+
/** Read the event log; a missing file reads as empty, malformed is flagged. */
|
|
91
|
+
async function readEvolutionEvents(io, path) {
|
|
92
|
+
const raw = await io.readText(path);
|
|
93
|
+
if (raw === null) return {
|
|
94
|
+
events: [],
|
|
95
|
+
malformed: false
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(raw);
|
|
99
|
+
if (!Array.isArray(parsed.events)) return {
|
|
100
|
+
events: [],
|
|
101
|
+
malformed: true
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
events: parsed.events.filter(isEventRecord),
|
|
105
|
+
malformed: false
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return {
|
|
109
|
+
events: [],
|
|
110
|
+
malformed: true
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
44
114
|
//#endregion
|
|
45
115
|
//#region ../evolution-core/src/prompts.ts
|
|
46
116
|
/**
|
|
@@ -276,8 +346,15 @@ new RegExp(String.raw`ignore\s+${FILLER}(?:previous|above|prior|all)\s+${FILLER}
|
|
|
276
346
|
* Feedback is durable through `ctx.evolutionIo` (when mounted) and skill
|
|
277
347
|
* feedback feeds `quality_score` / `quality_warn` on the usage record, so
|
|
278
348
|
* curator decisions can consume it deterministically.
|
|
349
|
+
*
|
|
350
|
+
* Persistence (rc.68): the EVENTS LOG (`evolution/events.json`, via
|
|
351
|
+
* `evolution-core/evolution-events.ts`) is the single source of truth —
|
|
352
|
+
* every increment appends one event under the write lock. `feedback.json` is
|
|
353
|
+
* a rebuildable BOOT CACHE (`{ version: 2, lastSeq, skills, sessions }`),
|
|
354
|
+
* never the truth; the in-memory state is the optimistic aggregate.
|
|
279
355
|
* @module @lmzhen/dsh-evolution-feedback
|
|
280
356
|
*/
|
|
357
|
+
const CACHE_VERSION = 2;
|
|
281
358
|
var EvolutionFeedback = class {
|
|
282
359
|
state = {
|
|
283
360
|
skills: {},
|
|
@@ -285,9 +362,13 @@ var EvolutionFeedback = class {
|
|
|
285
362
|
};
|
|
286
363
|
chain = Promise.resolve();
|
|
287
364
|
path;
|
|
365
|
+
eventsPath;
|
|
288
366
|
io;
|
|
289
367
|
constructor(io, home = process.env.DSH_HOME ?? join(homedir(), ".dsh"), pathOverride) {
|
|
290
|
-
if (io)
|
|
368
|
+
if (io) {
|
|
369
|
+
this.path = pathOverride ?? join(home, "evolution", "feedback.json");
|
|
370
|
+
this.eventsPath = eventsFile(home);
|
|
371
|
+
}
|
|
291
372
|
this.io = io;
|
|
292
373
|
}
|
|
293
374
|
mutate(task) {
|
|
@@ -297,26 +378,45 @@ var EvolutionFeedback = class {
|
|
|
297
378
|
}
|
|
298
379
|
async restore(io) {
|
|
299
380
|
const path = this.path;
|
|
300
|
-
|
|
381
|
+
const eventsPath = this.eventsPath;
|
|
382
|
+
if (!path || !eventsPath) return;
|
|
301
383
|
await this.mutate(async () => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
384
|
+
if (await io.readText(eventsPath) === null) {
|
|
385
|
+
const aggregate = parseAggregate(await io.readText(path));
|
|
386
|
+
if (aggregate) try {
|
|
387
|
+
await transactIo(io, eventsPath, (current) => {
|
|
388
|
+
if (current !== null) return Promise.resolve(current);
|
|
389
|
+
return Promise.resolve(JSON.stringify({
|
|
390
|
+
version: 1,
|
|
391
|
+
events: synthesizeFeedbackEvents(aggregate)
|
|
392
|
+
}, null, 2));
|
|
393
|
+
});
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
const { events } = await readEvolutionEvents(io, eventsPath);
|
|
397
|
+
const cache = parseCache(await io.readText(path));
|
|
398
|
+
const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
399
|
+
const truth = cache ? foldWithDelta(cache, events) : foldFeedbackState(events);
|
|
400
|
+
this.state = {
|
|
401
|
+
skills: {
|
|
402
|
+
...truth.skills,
|
|
403
|
+
...this.state.skills
|
|
404
|
+
},
|
|
405
|
+
sessions: {
|
|
406
|
+
...truth.sessions,
|
|
407
|
+
...this.state.sessions
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
if (maxSeq > 0 && (!cache || cache.lastSeq < maxSeq)) try {
|
|
411
|
+
await io.writeText(path, JSON.stringify({
|
|
412
|
+
version: CACHE_VERSION,
|
|
413
|
+
lastSeq: maxSeq,
|
|
414
|
+
...truth
|
|
415
|
+
}, null, 2));
|
|
316
416
|
} catch {}
|
|
317
417
|
});
|
|
318
418
|
}
|
|
319
|
-
record(target, rating, note, kind = "session"
|
|
419
|
+
record(target, rating, note, kind = "session") {
|
|
320
420
|
const mode = kind === "skill" ? "skills" : "sessions";
|
|
321
421
|
const table = this.state[mode];
|
|
322
422
|
const current = table[target] ?? {
|
|
@@ -326,28 +426,17 @@ var EvolutionFeedback = class {
|
|
|
326
426
|
current[rating] += 1;
|
|
327
427
|
if (note !== void 0) current.lastNote = note;
|
|
328
428
|
table[target] = current;
|
|
329
|
-
const recordIo =
|
|
330
|
-
const
|
|
331
|
-
if (!recordIo || !
|
|
429
|
+
const recordIo = this.io;
|
|
430
|
+
const eventsPath = this.eventsPath;
|
|
431
|
+
if (!recordIo || !eventsPath) return;
|
|
332
432
|
this.mutate(async () => {
|
|
333
433
|
try {
|
|
334
|
-
await
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const diskState = parseState(raw);
|
|
341
|
-
const diskTable = diskState[mode];
|
|
342
|
-
const diskRec = diskTable[target] ?? {
|
|
343
|
-
positive: 0,
|
|
344
|
-
negative: 0
|
|
345
|
-
};
|
|
346
|
-
diskRec[rating] += 1;
|
|
347
|
-
if (note !== void 0) diskRec.lastNote = note;
|
|
348
|
-
diskTable[target] = diskRec;
|
|
349
|
-
this.state = diskState;
|
|
350
|
-
return Promise.resolve(JSON.stringify(diskState, null, 2));
|
|
434
|
+
await appendEvolutionEvent(recordIo, eventsPath, {
|
|
435
|
+
type: "feedback",
|
|
436
|
+
target,
|
|
437
|
+
kind,
|
|
438
|
+
rating,
|
|
439
|
+
note
|
|
351
440
|
});
|
|
352
441
|
} catch (error) {}
|
|
353
442
|
});
|
|
@@ -369,26 +458,128 @@ var EvolutionFeedback = class {
|
|
|
369
458
|
waitIdle() {
|
|
370
459
|
return this.chain;
|
|
371
460
|
}
|
|
461
|
+
/** Rebuild the boot cache from the log truth (rc.68); best-effort. */
|
|
462
|
+
persistCache() {
|
|
463
|
+
const path = this.path;
|
|
464
|
+
const eventsPath = this.eventsPath;
|
|
465
|
+
const recordIo = this.io;
|
|
466
|
+
if (!path || !eventsPath || !recordIo) return Promise.resolve();
|
|
467
|
+
return this.mutate(async () => {
|
|
468
|
+
try {
|
|
469
|
+
const { events } = await readEvolutionEvents(recordIo, eventsPath);
|
|
470
|
+
const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
471
|
+
if (maxSeq === 0) return;
|
|
472
|
+
await recordIo.writeText(path, JSON.stringify({
|
|
473
|
+
version: CACHE_VERSION,
|
|
474
|
+
lastSeq: maxSeq,
|
|
475
|
+
...foldFeedbackState(events)
|
|
476
|
+
}, null, 2));
|
|
477
|
+
} catch {}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
372
480
|
};
|
|
373
|
-
/** Parse a
|
|
374
|
-
function
|
|
375
|
-
if (raw === null) return
|
|
376
|
-
skills: {},
|
|
377
|
-
sessions: {}
|
|
378
|
-
};
|
|
481
|
+
/** Parse a legacy aggregate (v1) or a v2 cache into a plain aggregate state. */
|
|
482
|
+
function parseAggregate(raw) {
|
|
483
|
+
if (raw === null) return null;
|
|
379
484
|
try {
|
|
380
485
|
const parsed = JSON.parse(raw);
|
|
486
|
+
const skills = isRecord(parsed.skills) ? parsed.skills : void 0;
|
|
487
|
+
const sessions = isRecord(parsed.sessions) ? parsed.sessions : void 0;
|
|
488
|
+
if (!skills && !sessions) return null;
|
|
381
489
|
return {
|
|
382
|
-
skills:
|
|
383
|
-
sessions:
|
|
490
|
+
skills: skills ?? {},
|
|
491
|
+
sessions: sessions ?? {}
|
|
384
492
|
};
|
|
385
493
|
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function parseCache(raw) {
|
|
498
|
+
if (raw === null) return null;
|
|
499
|
+
try {
|
|
500
|
+
const parsed = JSON.parse(raw);
|
|
501
|
+
if (parsed.version !== CACHE_VERSION || typeof parsed.lastSeq !== "number") return null;
|
|
502
|
+
if (!isRecord(parsed.skills) || !isRecord(parsed.sessions)) return null;
|
|
386
503
|
return {
|
|
387
|
-
|
|
388
|
-
|
|
504
|
+
lastSeq: parsed.lastSeq,
|
|
505
|
+
state: {
|
|
506
|
+
skills: parsed.skills,
|
|
507
|
+
sessions: parsed.sessions
|
|
508
|
+
}
|
|
389
509
|
};
|
|
510
|
+
} catch {
|
|
511
|
+
return null;
|
|
390
512
|
}
|
|
391
513
|
}
|
|
514
|
+
function isRecord(value) {
|
|
515
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
516
|
+
}
|
|
517
|
+
/** Fold all feedback events from zero (the truth view). */
|
|
518
|
+
function foldFeedbackState(events) {
|
|
519
|
+
const state = {
|
|
520
|
+
skills: {},
|
|
521
|
+
sessions: {}
|
|
522
|
+
};
|
|
523
|
+
for (const event of events) applyFeedbackEvent(state, event);
|
|
524
|
+
return state;
|
|
525
|
+
}
|
|
526
|
+
/** Fold events after the cache's lastSeq onto the cached aggregates. */
|
|
527
|
+
function foldWithDelta(cache, events) {
|
|
528
|
+
const state = {
|
|
529
|
+
skills: { ...cache.state.skills },
|
|
530
|
+
sessions: { ...cache.state.sessions }
|
|
531
|
+
};
|
|
532
|
+
for (const event of events) if (event.seq > cache.lastSeq) applyFeedbackEvent(state, event);
|
|
533
|
+
return state;
|
|
534
|
+
}
|
|
535
|
+
function applyFeedbackEvent(state, event) {
|
|
536
|
+
if (event.type !== "feedback") return;
|
|
537
|
+
const target = event.target;
|
|
538
|
+
if (target === void 0 || event.rating === void 0) return;
|
|
539
|
+
const table = event.kind === "skill" ? state.skills : state.sessions;
|
|
540
|
+
const record = table[target] ?? {
|
|
541
|
+
positive: 0,
|
|
542
|
+
negative: 0
|
|
543
|
+
};
|
|
544
|
+
record[event.rating] += 1;
|
|
545
|
+
if (event.note !== void 0) record.lastNote = event.note;
|
|
546
|
+
table[target] = record;
|
|
547
|
+
}
|
|
548
|
+
/** Synthesize one event per aggregate count unit, lastNote on the final event (migration). */
|
|
549
|
+
function synthesizeFeedbackEvents(aggregate) {
|
|
550
|
+
const events = [];
|
|
551
|
+
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
552
|
+
const emitTarget = (kind, target, record) => {
|
|
553
|
+
const first = events.length + 1;
|
|
554
|
+
for (let index = 0; index < record.positive; index += 1) events.push({
|
|
555
|
+
seq: events.length + 1,
|
|
556
|
+
at,
|
|
557
|
+
type: "feedback",
|
|
558
|
+
kind,
|
|
559
|
+
target,
|
|
560
|
+
rating: "positive"
|
|
561
|
+
});
|
|
562
|
+
for (let index = 0; index < record.negative; index += 1) events.push({
|
|
563
|
+
seq: events.length + 1,
|
|
564
|
+
at,
|
|
565
|
+
type: "feedback",
|
|
566
|
+
kind,
|
|
567
|
+
target,
|
|
568
|
+
rating: "negative"
|
|
569
|
+
});
|
|
570
|
+
if (record.lastNote !== void 0 && events.length >= first) {
|
|
571
|
+
const last = events.length - 1;
|
|
572
|
+
const final = events[last];
|
|
573
|
+
if (final) events[last] = {
|
|
574
|
+
...final,
|
|
575
|
+
note: record.lastNote
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
for (const [target, record] of Object.entries(aggregate.skills)) emitTarget("skill", target, record);
|
|
580
|
+
for (const [target, record] of Object.entries(aggregate.sessions)) emitTarget("session", target, record);
|
|
581
|
+
return events;
|
|
582
|
+
}
|
|
392
583
|
const name = "evolution-feedback";
|
|
393
584
|
const Config = z.object({
|
|
394
585
|
qualityWarnThreshold: z.number().default(-.25),
|
|
@@ -405,8 +596,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
405
596
|
const skillUsage = ctx.get("skillUsage");
|
|
406
597
|
if (skillUsage) {
|
|
407
598
|
const original = feedback.record.bind(feedback);
|
|
408
|
-
feedback.record = (target, rating, note, kind
|
|
409
|
-
original(target, rating, note, kind ?? "session"
|
|
599
|
+
feedback.record = (target, rating, note, kind) => {
|
|
600
|
+
original(target, rating, note, kind ?? "session");
|
|
410
601
|
if (kind === "skill") {
|
|
411
602
|
const score = feedback.score(target, "skill");
|
|
412
603
|
const warn = score < (rawConfig.qualityWarnThreshold ?? -.25);
|
|
@@ -417,7 +608,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
417
608
|
};
|
|
418
609
|
}
|
|
419
610
|
ctx.effect(() => () => {
|
|
420
|
-
return feedback.waitIdle();
|
|
611
|
+
return Promise.all([feedback.persistCache(), feedback.waitIdle()]);
|
|
421
612
|
}, "evolution-feedback.records");
|
|
422
613
|
}
|
|
423
614
|
//#endregion
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* Feedback is durable through `ctx.evolutionIo` (when mounted) and skill
|
|
5
5
|
* feedback feeds `quality_score` / `quality_warn` on the usage record, so
|
|
6
6
|
* curator decisions can consume it deterministically.
|
|
7
|
+
*
|
|
8
|
+
* Persistence (rc.68): the EVENTS LOG (`evolution/events.json`, via
|
|
9
|
+
* `evolution-core/evolution-events.ts`) is the single source of truth —
|
|
10
|
+
* every increment appends one event under the write lock. `feedback.json` is
|
|
11
|
+
* a rebuildable BOOT CACHE (`{ version: 2, lastSeq, skills, sessions }`),
|
|
12
|
+
* never the truth; the in-memory state is the optimistic aggregate.
|
|
7
13
|
* @module @deepseek-ai/dsh-evolution-feedback
|
|
8
14
|
*/
|
|
9
15
|
import type { Context } from '@deepseek-ai/cordis';
|
|
@@ -28,21 +34,25 @@ export declare class EvolutionFeedback {
|
|
|
28
34
|
private state;
|
|
29
35
|
private chain;
|
|
30
36
|
private readonly path?;
|
|
37
|
+
private readonly eventsPath?;
|
|
31
38
|
private readonly io;
|
|
32
39
|
constructor(io?: IoLike, home?: string, pathOverride?: string);
|
|
33
40
|
private mutate;
|
|
34
41
|
restore(io: IoLike): Promise<void>;
|
|
35
|
-
record(target: string, rating: 'positive' | 'negative', note?: string, kind?: 'skill' | 'session'
|
|
42
|
+
record(target: string, rating: 'positive' | 'negative', note?: string, kind?: 'skill' | 'session'): void;
|
|
36
43
|
score(target: string, kind?: 'skill' | 'session'): number;
|
|
37
44
|
snapshot(): FeedbackState;
|
|
38
45
|
/** Await the pending record-task chain (unload safety; rc.66). */
|
|
39
46
|
waitIdle(): Promise<unknown>;
|
|
47
|
+
/** Rebuild the boot cache from the log truth (rc.68); best-effort. */
|
|
48
|
+
persistCache(): Promise<void>;
|
|
40
49
|
}
|
|
41
50
|
export declare const name = "evolution-feedback";
|
|
42
51
|
export interface Config {
|
|
43
52
|
/** Score below which curator receives quality_warn for a skill. */
|
|
44
53
|
qualityWarnThreshold?: number;
|
|
45
|
-
/** Explicit
|
|
54
|
+
/** Explicit boot-cache file path; empty derives $DSH_HOME/evolution/feedback.json
|
|
55
|
+
* (the event log is its sibling `events.json`). */
|
|
46
56
|
path?: string;
|
|
47
57
|
}
|
|
48
58
|
export declare const Config: z<Config>;
|
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.68",
|
|
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.68",
|
|
41
|
+
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.68"
|
|
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.68",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.68",
|
|
47
|
+
"@lmzhen/dsh-skill-usage": "^0.1.0-rc.68"
|
|
48
48
|
}
|
|
49
49
|
}
|