@lmzhen/dsh-evolution-feedback 0.1.0-rc.68 → 0.1.0-rc.69

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
@@ -47,8 +47,13 @@ function eventsFile(home) {
47
47
  function isEventRecord(event) {
48
48
  return typeof event === "object" && event !== null && typeof event.seq === "number";
49
49
  }
50
- function parseEventList(raw) {
51
- if (raw === null) return [];
50
+ /**
51
+ * Parse an event log body. A missing file, a whitespace-only file (rc.69:
52
+ * rebuildable, NOT malformed) or a corrupt one reads as empty; corrupt content
53
+ * is still refused on append, never overwritten.
54
+ */
55
+ function parseEvolutionEvents(raw) {
56
+ if (raw === null || raw.trim() === "") return [];
52
57
  try {
53
58
  const parsed = JSON.parse(raw);
54
59
  if (!Array.isArray(parsed.events)) return [];
@@ -66,12 +71,12 @@ function parseEventList(raw) {
66
71
  async function appendEvolutionEvent(io, path, event) {
67
72
  let assigned = 0;
68
73
  await transactIo(io, path, (current) => {
69
- if (current !== null) try {
74
+ if (current !== null && current.trim() !== "") try {
70
75
  JSON.parse(current);
71
76
  } catch {
72
77
  return Promise.resolve(current);
73
78
  }
74
- const events = parseEventList(current);
79
+ const events = parseEvolutionEvents(current);
75
80
  const maxSeq = events.reduce((max, entry) => Math.max(max, entry.seq), 0);
76
81
  const record = {
77
82
  ...event,
@@ -87,10 +92,11 @@ async function appendEvolutionEvent(io, path, event) {
87
92
  if (assigned === 0) throw new Error(`evolution event log is malformed and was not touched: ${path}`);
88
93
  return assigned;
89
94
  }
90
- /** Read the event log; a missing file reads as empty, malformed is flagged. */
95
+ /** Read the event log; a missing/whitespace-only file reads as empty,
96
+ * corrupt content is flagged (and refused on append). */
91
97
  async function readEvolutionEvents(io, path) {
92
98
  const raw = await io.readText(path);
93
- if (raw === null) return {
99
+ if (raw === null || raw.trim() === "") return {
94
100
  events: [],
95
101
  malformed: false
96
102
  };
@@ -381,16 +387,11 @@ var EvolutionFeedback = class {
381
387
  const eventsPath = this.eventsPath;
382
388
  if (!path || !eventsPath) return;
383
389
  await this.mutate(async () => {
384
- if (await io.readText(eventsPath) === null) {
390
+ const rawEvents = await io.readText(eventsPath);
391
+ if (rawEvents === null || rawEvents.trim() === "") {
385
392
  const aggregate = parseAggregate(await io.readText(path));
386
393
  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
+ await migrateFeedbackEvents(io, eventsPath, aggregate);
394
395
  } catch {}
395
396
  }
396
397
  const { events } = await readEvolutionEvents(io, eventsPath);
@@ -478,6 +479,53 @@ var EvolutionFeedback = class {
478
479
  });
479
480
  }
480
481
  };
482
+ /** True when `existing` contains the legacy sequence as a contiguous run on
483
+ * its semantic fields (skip case). `seq` and `at` are excluded: after a merge
484
+ * the legacy events carry shifted seqs, and a re-synthesis stamps a different
485
+ * `at` — the semantic identity is type/kind/target/rating/note. A coincidental
486
+ * semantic match of an already-appended user sequence yields the identical
487
+ * aggregation, so the skip is harmless for counts and notes. */
488
+ function containsLegacySequence(existing, expected) {
489
+ if (expected.length === 0) return true;
490
+ for (let start = 0; start <= existing.length - expected.length; start += 1) {
491
+ let match = true;
492
+ for (let offset = 0; offset < expected.length; offset += 1) {
493
+ const a = expected[offset];
494
+ const b = existing[start + offset];
495
+ if (!a || !b || a.type !== b.type || a.kind !== b.kind || a.target !== b.target || a.rating !== b.rating || a.note !== b.note) {
496
+ match = false;
497
+ break;
498
+ }
499
+ }
500
+ if (match) return true;
501
+ }
502
+ return false;
503
+ }
504
+ /**
505
+ * Merge a legacy aggregate into the event log (rc.69): the expected synthetic
506
+ * sequence is APPENDED (seq-shifted) when the log does not already contain it
507
+ * — so a concurrent first writer's events AND the legacy history both
508
+ * survive; when the sequence is already present the migration was completed
509
+ * (by a first writer or by this path) and nothing is re-appended. Idempotent
510
+ * and race-safe (the search runs inside the same transact). Exported for the
511
+ * migration-race regression test.
512
+ */
513
+ async function migrateFeedbackEvents(io, eventsPath, aggregate) {
514
+ const expected = synthesizeFeedbackEvents(aggregate);
515
+ await transactIo(io, eventsPath, (current) => {
516
+ const existing = parseEvolutionEvents(current);
517
+ if (containsLegacySequence(existing, expected)) return Promise.resolve(current ?? "");
518
+ const maxSeq = existing.reduce((max, event) => Math.max(max, event.seq), 0);
519
+ const merged = [...existing, ...expected.map((event, index) => ({
520
+ ...event,
521
+ seq: maxSeq + index + 1
522
+ }))];
523
+ return Promise.resolve(JSON.stringify({
524
+ version: 1,
525
+ events: merged
526
+ }, null, 2));
527
+ });
528
+ }
481
529
  /** Parse a legacy aggregate (v1) or a v2 cache into a plain aggregate state. */
482
530
  function parseAggregate(raw) {
483
531
  if (raw === null) return null;
@@ -612,4 +660,4 @@ function apply(ctx, rawConfig = {}) {
612
660
  }, "evolution-feedback.records");
613
661
  }
614
662
  //#endregion
615
- export { Config, EvolutionFeedback, apply, name };
663
+ export { Config, EvolutionFeedback, apply, migrateFeedbackEvents, name };
@@ -47,12 +47,23 @@ export declare class EvolutionFeedback {
47
47
  /** Rebuild the boot cache from the log truth (rc.68); best-effort. */
48
48
  persistCache(): Promise<void>;
49
49
  }
50
+ /**
51
+ * Merge a legacy aggregate into the event log (rc.69): the expected synthetic
52
+ * sequence is APPENDED (seq-shifted) when the log does not already contain it
53
+ * — so a concurrent first writer's events AND the legacy history both
54
+ * survive; when the sequence is already present the migration was completed
55
+ * (by a first writer or by this path) and nothing is re-appended. Idempotent
56
+ * and race-safe (the search runs inside the same transact). Exported for the
57
+ * migration-race regression test.
58
+ */
59
+ export declare function migrateFeedbackEvents(io: IoLike, eventsPath: string, aggregate: FeedbackState): Promise<void>;
50
60
  export declare const name = "evolution-feedback";
51
61
  export interface Config {
52
62
  /** Score below which curator receives quality_warn for a skill. */
53
63
  qualityWarnThreshold?: number;
54
- /** Explicit boot-cache file path; empty derives $DSH_HOME/evolution/feedback.json
55
- * (the event log is its sibling `events.json`). */
64
+ /** Explicit boot-cache file path; empty derives $DSH_HOME/evolution/feedback.json.
65
+ * The event log always stays at $DSH_HOME/evolution/events.json (derived from
66
+ * home, never from this override). */
56
67
  path?: string;
57
68
  }
58
69
  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.68",
4
+ "version": "0.1.0-rc.69",
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.68",
41
- "@lmzhen/dsh-skill-usage": "^0.1.0-rc.68"
40
+ "@lmzhen/dsh-evolution-io": "^0.1.0-rc.69",
41
+ "@lmzhen/dsh-skill-usage": "^0.1.0-rc.69"
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.68",
46
- "@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.68",
47
- "@lmzhen/dsh-skill-usage": "^0.1.0-rc.68"
45
+ "@lmzhen/dsh-evolution-io": "^0.1.0-rc.69",
46
+ "@lmzhen/dsh-evolution-io-node": "^0.1.0-rc.69",
47
+ "@lmzhen/dsh-skill-usage": "^0.1.0-rc.69"
48
48
  }
49
49
  }