@lmzhen/dsh-evolution-feedback 0.3.66 → 0.3.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 CHANGED
@@ -48,11 +48,16 @@ var EvolutionFeedback = class EvolutionFeedback {
48
48
  durableNote = /* @__PURE__ */ new Map();
49
49
  /** P2-32 (v11): process-level bound — every feedbacked session would
50
50
  * otherwise keep one entry for the whole process lifetime (a name + note
51
- * string per session); cap both maps and drop the oldest on overflow. */
51
+ * string per session); cap this map and drop the earliest-INSERTED entry on
52
+ * overflow (Map iteration order is insertion order, and re-setting an
53
+ * existing key does not refresh its position, so the eviction is FIFO rather
54
+ * than least-recently-used). `warnedMessages` carries the same bound through
55
+ * `WARNED_CAP` below. */
52
56
  static NOTE_CAP = 512;
53
57
  /** V5-32 (0.3.31): fire-and-forget append failures are warn-once per unique
54
58
  * message — a persistent refusal (e.g. a future-version log) must not spam
55
- * the log on every user feedback entry (family posture: process-level once). */
59
+ * the log on every user feedback entry (family posture: process-level once).
60
+ * Bounded like `durableNote`, with the same FIFO eviction. */
56
61
  warnedMessages = /* @__PURE__ */ new Set();
57
62
  WARNED_CAP = 512;
58
63
  /** V5-29 (0.3.31): invoked after a FAILED append rolled back, so a caller
@@ -86,7 +91,7 @@ var EvolutionFeedback = class EvolutionFeedback {
86
91
  const rawEvents = await io.readText(eventsPath);
87
92
  const archiveNames = await listEventArchives(io, eventsPath);
88
93
  if ((rawEvents === null || rawEvents.trim() === "") && archiveNames.length === 0) {
89
- const aggregate = parseAggregate(await io.readText(path));
94
+ const aggregate = parseAggregate(await io.readText(path), this.warn);
90
95
  if (aggregate) try {
91
96
  await migrateFeedbackEvents(io, eventsPath, aggregate);
92
97
  } catch {}
@@ -293,13 +298,34 @@ async function migrateFeedbackEvents(io, eventsPath, aggregate) {
293
298
  }, null, 2));
294
299
  });
295
300
  }
296
- /** Parse a legacy aggregate (v1) or a v2 cache into a plain aggregate state. */
297
- function parseAggregate(raw) {
301
+ /** Parse a legacy aggregate (v1) or a v2 cache into a plain aggregate state.
302
+ * V24-05 (v24): every record passes the shared per-record sanitizer (same
303
+ * validation as `parseCache` — S6.4), and migrated counts are clamped so one
304
+ * corrupted field cannot expand into an unbounded event array at boot. */
305
+ const MAX_MIGRATED_EVENTS_PER_RECORD = 1e4;
306
+ function parseAggregate(raw, warn = () => {}) {
298
307
  if (raw === null) return null;
299
308
  try {
300
309
  const parsed = JSON.parse(raw);
301
- const skills = isRecord(parsed.skills) ? parsed.skills : void 0;
302
- const sessions = isRecord(parsed.sessions) ? parsed.sessions : void 0;
310
+ const clamp = (count, target, field) => {
311
+ if (count > MAX_MIGRATED_EVENTS_PER_RECORD) {
312
+ warn(`evolution-feedback: migration clamps ${field}=${count} for "${target}" to ${MAX_MIGRATED_EVENTS_PER_RECORD} — the on-disk aggregate is not trustworthy at that scale`);
313
+ return MAX_MIGRATED_EVENTS_PER_RECORD;
314
+ }
315
+ return count;
316
+ };
317
+ const sanitizeSection = (section, kind) => {
318
+ if (!isRecord(section)) return void 0;
319
+ const cleaned = sanitizeCacheRecords(section, kind, warn);
320
+ for (const [target, record] of Object.entries(cleaned)) if (record.positive > MAX_MIGRATED_EVENTS_PER_RECORD || record.negative > MAX_MIGRATED_EVENTS_PER_RECORD) cleaned[target] = {
321
+ ...record,
322
+ positive: clamp(record.positive, target, "positive"),
323
+ negative: clamp(record.negative, target, "negative")
324
+ };
325
+ return cleaned;
326
+ };
327
+ const skills = sanitizeSection(parsed.skills, "skill");
328
+ const sessions = sanitizeSection(parsed.sessions, "session");
303
329
  if (!skills && !sessions) return null;
304
330
  return {
305
331
  skills: skills ?? {},
@@ -52,11 +52,16 @@ export declare class EvolutionFeedback {
52
52
  private readonly durableNote;
53
53
  /** P2-32 (v11): process-level bound — every feedbacked session would
54
54
  * otherwise keep one entry for the whole process lifetime (a name + note
55
- * string per session); cap both maps and drop the oldest on overflow. */
55
+ * string per session); cap this map and drop the earliest-INSERTED entry on
56
+ * overflow (Map iteration order is insertion order, and re-setting an
57
+ * existing key does not refresh its position, so the eviction is FIFO rather
58
+ * than least-recently-used). `warnedMessages` carries the same bound through
59
+ * `WARNED_CAP` below. */
56
60
  private static readonly NOTE_CAP;
57
61
  /** V5-32 (0.3.31): fire-and-forget append failures are warn-once per unique
58
62
  * message — a persistent refusal (e.g. a future-version log) must not spam
59
- * the log on every user feedback entry (family posture: process-level once). */
63
+ * the log on every user feedback entry (family posture: process-level once).
64
+ * Bounded like `durableNote`, with the same FIFO eviction. */
60
65
  private readonly warnedMessages;
61
66
  private readonly WARNED_CAP;
62
67
  /** V5-29 (0.3.31): invoked after a FAILED append rolled back, so a caller
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.66",
4
+ "version": "0.3.68",
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.66"
34
+ "@lmzhen/dsh-evolution-core": "^0.3.68"
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.66",
40
- "@lmzhen/dsh-skill-usage": "^0.3.66"
39
+ "@lmzhen/dsh-evolution-io": "^0.3.68",
40
+ "@lmzhen/dsh-skill-usage": "^0.3.68"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
44
- "@lmzhen/dsh-evolution-io": "^0.3.66",
45
- "@lmzhen/dsh-evolution-io-node": "^0.3.66",
46
- "@lmzhen/dsh-skill-usage": "^0.3.66"
44
+ "@lmzhen/dsh-evolution-io": "^0.3.68",
45
+ "@lmzhen/dsh-evolution-io-node": "^0.3.68",
46
+ "@lmzhen/dsh-skill-usage": "^0.3.68"
47
47
  }
48
48
  }