@lmzhen/dsh-evolution-feedback 0.3.16 → 0.3.18
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 +109 -35
- package/lib/types/index.d.ts +5 -2
- package/package.json +6 -6
package/lib/index.js
CHANGED
|
@@ -38,6 +38,10 @@ function evolutionIoAdapter(provider) {
|
|
|
38
38
|
isSymlink: (path) => {
|
|
39
39
|
const io = provider();
|
|
40
40
|
return io.isSymlink ? io.isSymlink(path) : Promise.resolve(null);
|
|
41
|
+
},
|
|
42
|
+
mtime: (path) => {
|
|
43
|
+
const io = provider();
|
|
44
|
+
return io.mtime ? io.mtime(path) : Promise.resolve(null);
|
|
41
45
|
}
|
|
42
46
|
};
|
|
43
47
|
}
|
|
@@ -2870,11 +2874,15 @@ var EvolutionFeedback = class {
|
|
|
2870
2874
|
path;
|
|
2871
2875
|
eventsPath;
|
|
2872
2876
|
io;
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2877
|
+
warn;
|
|
2878
|
+
constructor(io, home = process.env.DSH_HOME ?? join(homedir(), ".dsh"), pathOverride, warn = () => {}) {
|
|
2879
|
+
this.path = pathOverride ?? join(home, "evolution", "feedback.json");
|
|
2880
|
+
this.eventsPath = eventsFile(home);
|
|
2881
|
+
this.io = io;
|
|
2882
|
+
this.warn = warn;
|
|
2883
|
+
}
|
|
2884
|
+
/** Bind the evolution IO backend after construction (S6.4 deferred binding). */
|
|
2885
|
+
attachIo(io) {
|
|
2878
2886
|
this.io = io;
|
|
2879
2887
|
}
|
|
2880
2888
|
mutate(task) {
|
|
@@ -2896,11 +2904,11 @@ var EvolutionFeedback = class {
|
|
|
2896
2904
|
} catch {}
|
|
2897
2905
|
}
|
|
2898
2906
|
const { events } = await readEvolutionTimeline(io, eventsPath);
|
|
2899
|
-
const cache = parseCache(await io.readText(path));
|
|
2907
|
+
const cache = parseCache(await io.readText(path), this.warn);
|
|
2900
2908
|
const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
2901
2909
|
const floor = events[0]?.seq ?? 0;
|
|
2902
2910
|
const usableCache = cache && cache.lastSeq >= floor - 1 ? cache : null;
|
|
2903
|
-
const truth = usableCache ? foldWithDelta(usableCache, events) : foldFeedbackState(events);
|
|
2911
|
+
const truth = usableCache ? foldWithDelta(usableCache, events, this.warn) : foldFeedbackState(events, this.warn);
|
|
2904
2912
|
this.state = {
|
|
2905
2913
|
skills: {
|
|
2906
2914
|
...truth.skills,
|
|
@@ -2927,6 +2935,7 @@ var EvolutionFeedback = class {
|
|
|
2927
2935
|
positive: 0,
|
|
2928
2936
|
negative: 0
|
|
2929
2937
|
};
|
|
2938
|
+
const previousNote = current.lastNote;
|
|
2930
2939
|
current[rating] += 1;
|
|
2931
2940
|
if (note !== void 0) current.lastNote = note;
|
|
2932
2941
|
table[target] = current;
|
|
@@ -2942,7 +2951,14 @@ var EvolutionFeedback = class {
|
|
|
2942
2951
|
rating,
|
|
2943
2952
|
note
|
|
2944
2953
|
}) % CACHE_SNAP_EVERY === 0) await this.writeCacheNow();
|
|
2945
|
-
} catch (error) {
|
|
2954
|
+
} catch (error) {
|
|
2955
|
+
const rollback = table[target];
|
|
2956
|
+
if (rollback) {
|
|
2957
|
+
rollback[rating] -= 1;
|
|
2958
|
+
if (note !== void 0) if (previousNote === void 0) delete rollback.lastNote;
|
|
2959
|
+
else rollback.lastNote = previousNote;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2946
2962
|
});
|
|
2947
2963
|
}
|
|
2948
2964
|
score(target, kind = "session") {
|
|
@@ -2972,11 +2988,12 @@ var EvolutionFeedback = class {
|
|
|
2972
2988
|
const { events } = await readEvolutionTimeline(recordIo, eventsPath);
|
|
2973
2989
|
const maxSeq = events.reduce((max, event) => Math.max(max, event.seq), 0);
|
|
2974
2990
|
if (maxSeq === 0) return;
|
|
2975
|
-
|
|
2991
|
+
const body = JSON.stringify({
|
|
2976
2992
|
version: CACHE_VERSION,
|
|
2977
2993
|
lastSeq: maxSeq,
|
|
2978
|
-
...foldFeedbackState(events)
|
|
2979
|
-
}, null, 2)
|
|
2994
|
+
...foldFeedbackState(events, this.warn)
|
|
2995
|
+
}, null, 2);
|
|
2996
|
+
await recordIo.writeText(path, body);
|
|
2980
2997
|
} catch {}
|
|
2981
2998
|
}
|
|
2982
2999
|
/** Rebuild the boot cache from the log truth (rc.68); best-effort, queued
|
|
@@ -3050,48 +3067,87 @@ function parseAggregate(raw) {
|
|
|
3050
3067
|
return null;
|
|
3051
3068
|
}
|
|
3052
3069
|
}
|
|
3053
|
-
function parseCache(raw) {
|
|
3070
|
+
function parseCache(raw, warn = () => {}) {
|
|
3054
3071
|
if (raw === null) return null;
|
|
3055
3072
|
try {
|
|
3056
3073
|
const parsed = JSON.parse(raw);
|
|
3057
|
-
if (parsed.version !== CACHE_VERSION || typeof parsed.lastSeq !== "number") return null;
|
|
3074
|
+
if (parsed.version !== CACHE_VERSION || typeof parsed.lastSeq !== "number" || !Number.isFinite(parsed.lastSeq)) return null;
|
|
3058
3075
|
if (!isRecord(parsed.skills) || !isRecord(parsed.sessions)) return null;
|
|
3059
3076
|
return {
|
|
3060
3077
|
lastSeq: parsed.lastSeq,
|
|
3061
3078
|
state: {
|
|
3062
|
-
skills: parsed.skills,
|
|
3063
|
-
sessions: parsed.sessions
|
|
3079
|
+
skills: sanitizeCacheRecords(parsed.skills, "skill", warn),
|
|
3080
|
+
sessions: sanitizeCacheRecords(parsed.sessions, "session", warn)
|
|
3064
3081
|
}
|
|
3065
3082
|
};
|
|
3066
3083
|
} catch {
|
|
3067
3084
|
return null;
|
|
3068
3085
|
}
|
|
3069
3086
|
}
|
|
3087
|
+
/** Per-record numeric-domain validation (S6.4): a record whose `positive` or
|
|
3088
|
+
* `negative` is not a finite number >= 0, or whose `lastNote` is not a string,
|
|
3089
|
+
* would fold as NaN into the usage aggregate — skip it with a warn instead of
|
|
3090
|
+
* corrupting the state. A record with no valid count is dropped entirely. */
|
|
3091
|
+
function sanitizeCacheRecords(input, kind, warn) {
|
|
3092
|
+
const out = {};
|
|
3093
|
+
for (const [target, value] of Object.entries(input)) {
|
|
3094
|
+
const record = sanitizeFeedbackRecord(value, target, kind, warn);
|
|
3095
|
+
if (record) out[target] = record;
|
|
3096
|
+
}
|
|
3097
|
+
return out;
|
|
3098
|
+
}
|
|
3099
|
+
function sanitizeFeedbackRecord(value, target, kind, warn) {
|
|
3100
|
+
if (!isRecord(value)) {
|
|
3101
|
+
warn(`evolution-feedback: skipping cache record for ${kind} "${target}": not a record`);
|
|
3102
|
+
return null;
|
|
3103
|
+
}
|
|
3104
|
+
const positive = value.positive;
|
|
3105
|
+
const negative = value.negative;
|
|
3106
|
+
const note = value.lastNote;
|
|
3107
|
+
const validCount = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
3108
|
+
if (!validCount(positive) || !validCount(negative)) {
|
|
3109
|
+
warn(`evolution-feedback: skipping cache record for ${kind} "${target}": positive/negative must be finite numbers >= 0`);
|
|
3110
|
+
return null;
|
|
3111
|
+
}
|
|
3112
|
+
if (note !== void 0 && typeof note !== "string") {
|
|
3113
|
+
warn(`evolution-feedback: skipping cache record for ${kind} "${target}": lastNote must be a string`);
|
|
3114
|
+
return null;
|
|
3115
|
+
}
|
|
3116
|
+
return {
|
|
3117
|
+
positive,
|
|
3118
|
+
negative,
|
|
3119
|
+
...note !== void 0 ? { lastNote: note } : {}
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3070
3122
|
function isRecord(value) {
|
|
3071
3123
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3072
3124
|
}
|
|
3073
3125
|
/** Fold all feedback events from zero (the truth view). */
|
|
3074
|
-
function foldFeedbackState(events) {
|
|
3126
|
+
function foldFeedbackState(events, warn = () => {}) {
|
|
3075
3127
|
const state = {
|
|
3076
3128
|
skills: {},
|
|
3077
3129
|
sessions: {}
|
|
3078
3130
|
};
|
|
3079
|
-
for (const event of events) applyFeedbackEvent(state, event);
|
|
3131
|
+
for (const event of events) applyFeedbackEvent(state, event, warn);
|
|
3080
3132
|
return state;
|
|
3081
3133
|
}
|
|
3082
3134
|
/** Fold events after the cache's lastSeq onto the cached aggregates. */
|
|
3083
|
-
function foldWithDelta(cache, events) {
|
|
3135
|
+
function foldWithDelta(cache, events, warn = () => {}) {
|
|
3084
3136
|
const state = {
|
|
3085
3137
|
skills: { ...cache.state.skills },
|
|
3086
3138
|
sessions: { ...cache.state.sessions }
|
|
3087
3139
|
};
|
|
3088
|
-
for (const event of events) if (event.seq > cache.lastSeq) applyFeedbackEvent(state, event);
|
|
3140
|
+
for (const event of events) if (event.seq > cache.lastSeq) applyFeedbackEvent(state, event, warn);
|
|
3089
3141
|
return state;
|
|
3090
3142
|
}
|
|
3091
|
-
function applyFeedbackEvent(state, event) {
|
|
3143
|
+
function applyFeedbackEvent(state, event, warn = () => {}) {
|
|
3092
3144
|
if (event.type !== "feedback") return;
|
|
3145
|
+
if (event.rating !== "positive" && event.rating !== "negative") {
|
|
3146
|
+
warn(`evolution-feedback: skipping feedback event with invalid rating: ${String(event.rating)}`);
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3093
3149
|
const target = event.target;
|
|
3094
|
-
if (target === void 0
|
|
3150
|
+
if (target === void 0) return;
|
|
3095
3151
|
const table = event.kind === "skill" ? state.skills : state.sessions;
|
|
3096
3152
|
const record = table[target] ?? {
|
|
3097
3153
|
positive: 0,
|
|
@@ -3101,7 +3157,9 @@ function applyFeedbackEvent(state, event) {
|
|
|
3101
3157
|
if (event.note !== void 0) record.lastNote = event.note;
|
|
3102
3158
|
table[target] = record;
|
|
3103
3159
|
}
|
|
3104
|
-
/** Synthesize one event per aggregate count unit, lastNote on the final event
|
|
3160
|
+
/** Synthesize one event per aggregate count unit, lastNote on the final event
|
|
3161
|
+
* (migration). A zero-count record still carrying a lastNote would otherwise
|
|
3162
|
+
* drop the note — emit a dedicated note event (S6.4) so it survives. */
|
|
3105
3163
|
function synthesizeFeedbackEvents(aggregate) {
|
|
3106
3164
|
const events = [];
|
|
3107
3165
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3123,14 +3181,22 @@ function synthesizeFeedbackEvents(aggregate) {
|
|
|
3123
3181
|
target,
|
|
3124
3182
|
rating: "negative"
|
|
3125
3183
|
});
|
|
3126
|
-
if (record.lastNote !== void 0
|
|
3184
|
+
if (record.lastNote !== void 0) if (events.length >= first) {
|
|
3127
3185
|
const last = events.length - 1;
|
|
3128
3186
|
const final = events[last];
|
|
3129
3187
|
if (final) events[last] = {
|
|
3130
3188
|
...final,
|
|
3131
3189
|
note: record.lastNote
|
|
3132
3190
|
};
|
|
3133
|
-
}
|
|
3191
|
+
} else events.push({
|
|
3192
|
+
seq: events.length + 1,
|
|
3193
|
+
at,
|
|
3194
|
+
type: "feedback",
|
|
3195
|
+
kind,
|
|
3196
|
+
target,
|
|
3197
|
+
rating: "positive",
|
|
3198
|
+
note: record.lastNote
|
|
3199
|
+
});
|
|
3134
3200
|
};
|
|
3135
3201
|
for (const [target, record] of Object.entries(aggregate.skills)) emitTarget("skill", target, record);
|
|
3136
3202
|
for (const [target, record] of Object.entries(aggregate.sessions)) emitTarget("session", target, record);
|
|
@@ -3142,27 +3208,35 @@ const Config = z.object({
|
|
|
3142
3208
|
path: z.string().default("")
|
|
3143
3209
|
});
|
|
3144
3210
|
function apply(ctx, rawConfig = {}) {
|
|
3145
|
-
const
|
|
3146
|
-
|
|
3147
|
-
const feedback = new EvolutionFeedback(io, process.env.DSH_HOME ?? join(homedir(), ".dsh"), rawConfig.path || void 0);
|
|
3148
|
-
if (io) feedback.restore(io).catch((error) => {
|
|
3149
|
-
ctx.logger.warn(error);
|
|
3211
|
+
const feedback = new EvolutionFeedback(void 0, process.env.DSH_HOME ?? join(homedir(), ".dsh"), rawConfig.path || void 0, (message) => {
|
|
3212
|
+
ctx.logger.warn(message);
|
|
3150
3213
|
});
|
|
3151
3214
|
ctx.provide("evolutionFeedback", feedback);
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
const
|
|
3215
|
+
ctx.inject(["evolutionIo"], (ioCtx) => {
|
|
3216
|
+
const ioRegistry = ioCtx.evolutionIo;
|
|
3217
|
+
const io = evolutionIoAdapter(() => ioRegistry.provider());
|
|
3218
|
+
feedback.attachIo(io);
|
|
3219
|
+
feedback.restore(io).catch((error) => {
|
|
3220
|
+
ioCtx.logger.warn(error);
|
|
3221
|
+
});
|
|
3222
|
+
});
|
|
3223
|
+
const baseRecord = feedback.record.bind(feedback);
|
|
3224
|
+
let qualityWired = false;
|
|
3225
|
+
ctx.inject(["skillUsage"], (skillCtx) => {
|
|
3226
|
+
if (qualityWired) return;
|
|
3227
|
+
qualityWired = true;
|
|
3228
|
+
const skillUsage = skillCtx.skillUsage;
|
|
3155
3229
|
feedback.record = (target, rating, note, kind) => {
|
|
3156
|
-
|
|
3230
|
+
baseRecord(target, rating, note, kind ?? "session");
|
|
3157
3231
|
if (kind === "skill") {
|
|
3158
3232
|
const score = feedback.score(target, "skill");
|
|
3159
3233
|
const warn = score < (rawConfig.qualityWarnThreshold ?? -.25);
|
|
3160
3234
|
skillUsage.setQuality(target, score, warn).catch((error) => {
|
|
3161
|
-
|
|
3235
|
+
skillCtx.logger.warn(error);
|
|
3162
3236
|
});
|
|
3163
3237
|
}
|
|
3164
3238
|
};
|
|
3165
|
-
}
|
|
3239
|
+
});
|
|
3166
3240
|
ctx.effect(() => () => {
|
|
3167
3241
|
return Promise.all([feedback.persistCache(), feedback.waitIdle()]);
|
|
3168
3242
|
}, "evolution-feedback.records");
|
package/lib/types/index.d.ts
CHANGED
|
@@ -35,8 +35,11 @@ export declare class EvolutionFeedback {
|
|
|
35
35
|
private chain;
|
|
36
36
|
private readonly path?;
|
|
37
37
|
private readonly eventsPath?;
|
|
38
|
-
private
|
|
39
|
-
|
|
38
|
+
private io;
|
|
39
|
+
private warn;
|
|
40
|
+
constructor(io?: IoLike, home?: string, pathOverride?: string, warn?: (message: string) => void);
|
|
41
|
+
/** Bind the evolution IO backend after construction (S6.4 deferred binding). */
|
|
42
|
+
attachIo(io: IoLike): void;
|
|
40
43
|
private mutate;
|
|
41
44
|
restore(io: IoLike): Promise<void>;
|
|
42
45
|
record(target: string, rating: 'positive' | 'negative', note?: string, kind?: 'skill' | 'session'): void;
|
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.3.
|
|
4
|
+
"version": "0.3.18",
|
|
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.3.
|
|
41
|
-
"@lmzhen/dsh-skill-usage": "^0.3.
|
|
40
|
+
"@lmzhen/dsh-evolution-io": "^0.3.18",
|
|
41
|
+
"@lmzhen/dsh-skill-usage": "^0.3.18"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
45
|
-
"@lmzhen/dsh-evolution-io": "^0.3.
|
|
46
|
-
"@lmzhen/dsh-evolution-io-node": "^0.3.
|
|
47
|
-
"@lmzhen/dsh-skill-usage": "^0.3.
|
|
45
|
+
"@lmzhen/dsh-evolution-io": "^0.3.18",
|
|
46
|
+
"@lmzhen/dsh-evolution-io-node": "^0.3.18",
|
|
47
|
+
"@lmzhen/dsh-skill-usage": "^0.3.18"
|
|
48
48
|
}
|
|
49
49
|
}
|