@lmzhen/dsh-evolution-curator 0.17.1 → 0.18.1

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
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
4
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import z from "@deepseek-ai/schemastery";
6
- import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_SKILL_LIMITS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, INSTANCE_KEYS, MAX_TIMER_DELAY_MS, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, emptyRecord, entryTarget, evolutionHome, evolutionIoAdapter, foldCuratorFields, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, paramRowId, parseCuratorNominations, parseFrontmatter, policyStageLimits, probeList, probeMtime, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, sessionLastEventTime, transactIo, updateSuppressedNames, usageObserved } from "@lmzhen/dsh-evolution-core";
6
+ import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_SKILL_LIMITS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, INSTANCE_KEYS, MAX_TIMER_DELAY_MS, PROMPT_BUNDLE, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampOnce, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, emptyRecord, entryTarget, evolutionHome, evolutionIoAdapter, foldCuratorFields, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, paramRowId, parseCuratorNominations, parseFrontmatter, pickWithPolicy, policyStageLimits, probeList, probeMtime, probeText, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, reportTime, sessionLastEventTime, transactIo, updateSuppressedNames, usageObserved, userSetKeys, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
7
7
  //#region lib/types/index.js
8
8
  /**
9
9
  * Deterministic skill lifecycle curator with interval gate and archive.
@@ -80,6 +80,10 @@ var EvolutionCurator = class extends Service {
80
80
  bootGraceSeconds;
81
81
  /** Keys whose supplied value had to be corrected; each warns once (G3.1). */
82
82
  clampedKeys = /* @__PURE__ */ new Set();
83
+ /** The clamp warning sink — the LOGIC lives in core's `clampOnce` (S2.9/T3-10), the prefix here. */
84
+ clampWarn = (message) => {
85
+ this.ctx.logger.warn("evolution-curator: " + message);
86
+ };
83
87
  lastRun = 0;
84
88
  timer;
85
89
  /** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
@@ -113,9 +117,10 @@ var EvolutionCurator = class extends Service {
113
117
  ctx.inject(["settings"], (injected) => {
114
118
  const settings = injected.settings;
115
119
  if (typeof settings?.configure !== "function") return;
116
- const disposer = settings.configure({ auto: false });
120
+ const disposer = settings.configure({ auto: false }, ctx.fiber);
117
121
  if (typeof disposer === "function") ctx.effect(() => disposer, "evolution-curator: settings presentation");
118
122
  });
123
+ if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to run the curator");
119
124
  this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
120
125
  this.skills = newSkillLibrary({
121
126
  config,
@@ -137,7 +142,7 @@ var EvolutionCurator = class extends Service {
137
142
  this.manageUnmanaged = config.manageUnmanaged ?? false;
138
143
  this.pruneBuiltins = config.pruneBuiltins ?? false;
139
144
  this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
140
- this.bootGraceSeconds = this.clampOnce("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
145
+ this.bootGraceSeconds = clampOnce(this.clampedKeys, this.clampWarn, "bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
141
146
  const archiveAtBoot = this.rowNumber("archiveAfterDays", DEFAULT_ARCHIVE_AFTER_DAYS, 1);
142
147
  const staleAtBoot = this.rowNumber("staleAfterDays", DEFAULT_STALE_AFTER_DAYS, 1);
143
148
  if (archiveAtBoot < staleAtBoot) this.ctx.logger.warn(`evolution-curator: archiveAfterDays (${archiveAtBoot}) < staleAfterDays (${staleAtBoot}); using staleAfterDays as the archive threshold`);
@@ -163,26 +168,6 @@ var EvolutionCurator = class extends Service {
163
168
  if (config.autoStart ?? true) this.start();
164
169
  }
165
170
  /**
166
- * Clamp one supplied numeric value, warning once per key (G3.1).
167
- * @param key - the config key, for the warning.
168
- * @param value - the value as supplied by the deployment or the user.
169
- * @param fallback - the package default a corrected value falls back to.
170
- * @param min - the smallest value the engine can act on.
171
- * @param max - the largest value the carrier can hold, when bounded.
172
- * @returns the value the engine may use.
173
- */
174
- clampOnce(key, value, fallback, min, max) {
175
- const result = clampedNumber(value, fallback, max === void 0 ? { min } : {
176
- min,
177
- max
178
- });
179
- if (value !== void 0 && result !== value && !this.clampedKeys.has(key)) {
180
- this.clampedKeys.add(key);
181
- this.ctx.logger.warn(`evolution-curator: ${key} provided an invalid value; falling back to the default`);
182
- }
183
- return result;
184
- }
185
- /**
186
171
  * One live numeric row field at USE time (G1 §8.3): the row config carries the
187
172
  * platform-resolved layers (schema default < bundle row < profile override) and the
188
173
  * field stays a live reference, so a committed edit applies without a watcher.
@@ -202,7 +187,7 @@ var EvolutionCurator = class extends Service {
202
187
  * @returns the value the engine may use.
203
188
  */
204
189
  rowNumber(key, fallback, min, max) {
205
- return this.clampOnce(key, this.liveNumber(key), fallback, min, max);
190
+ return clampOnce(this.clampedKeys, this.clampWarn, key, this.liveNumber(key), fallback, min, max);
206
191
  }
207
192
  /**
208
193
  * One live boolean row field at USE time.
@@ -235,20 +220,6 @@ var EvolutionCurator = class extends Service {
235
220
  healthChurnMinPatches: number("healthChurnMinPatches", DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1)
236
221
  };
237
222
  }
238
- /**
239
- * The keys the user set in this row's namespace (G1 §8.4-A).
240
- *
241
- * This is the ONLY remaining read of the settings user layer, and it reads KEY
242
- * NAMES, never values: the platform resolves the user layer into the row config,
243
- * so a value is needed from `config` — but the precedence rule (user > policy)
244
- * needs to know WHICH keys the user set, or a deployment policy would silently
245
- * override the user.
246
- * @returns the keys present in this row's user layer.
247
- */
248
- userSetKeys() {
249
- const entry = this.ctx.get("settings")?.describe?.({ redactSecrets: false }).find((item) => item.ns === paramRowId("evolution-curator"));
250
- return new Set(Object.keys(entry?.user ?? {}));
251
- }
252
223
  /** The policy-snapshot carrier for this group (unmounted service = no policy). */
253
224
  policyFields() {
254
225
  return this.ctx.get("evolutionPolicy")?.get() ?? {};
@@ -265,11 +236,8 @@ var EvolutionCurator = class extends Service {
265
236
  settings() {
266
237
  const snapshot = this.policyFields();
267
238
  const row = this.rowSettings();
268
- const userSet = this.userSetKeys();
269
- const pick = (key) => {
270
- if (userSet.has(key)) return row[key];
271
- return snapshot[key] ?? row[key];
272
- };
239
+ const userSet = userSetKeys(this.ctx, paramRowId("evolution-curator"));
240
+ const pick = (key) => pickWithPolicy(userSet, row, snapshot, key);
273
241
  const number = (key, min, max) => {
274
242
  const lower = row[key];
275
243
  return clampedNumber(pick(key), lower, max === void 0 ? { min } : {
@@ -436,6 +404,7 @@ var EvolutionCurator = class extends Service {
436
404
  "Return a YAML summary with consolidations and prunings lists. Nominate only actions whose archival/merge is clearly safe."
437
405
  ].join("\n");
438
406
  const settings = this.settings();
407
+ const llmSignal = AbortSignal.timeout(settings.curatorReviewTimeoutMs);
439
408
  try {
440
409
  const assembler = new BlockAssembler();
441
410
  for await (const chunk of llm.stream({
@@ -453,7 +422,7 @@ var EvolutionCurator = class extends Service {
453
422
  }
454
423
  })],
455
424
  maxTokens: settings.curatorReviewMaxTokens,
456
- signal: AbortSignal.timeout(settings.curatorReviewTimeoutMs)
425
+ signal: llmSignal
457
426
  })) assembler.push(chunk);
458
427
  const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
459
428
  return {
@@ -462,8 +431,12 @@ var EvolutionCurator = class extends Service {
462
431
  warnings: parsed.warnings
463
432
  };
464
433
  } catch (error) {
465
- this.ctx.logger.warn(`evolution-curator: LLM nomination pass failed: ${error instanceof Error ? error.message : String(error)}`);
466
- return empty;
434
+ const failure = llmSignal.aborted ? `LLM nomination pass timed out after ${settings.curatorReviewTimeoutMs} ms — no nominations this run (NOT "nothing to nominate": the pass did not complete)` : `LLM nomination pass failed: ${error instanceof Error ? error.message : String(error)} — no nominations this run (NOT "nothing to nominate": the pass did not complete)`;
435
+ this.ctx.logger.warn(`evolution-curator: ${failure}`);
436
+ return {
437
+ ...empty,
438
+ warnings: [failure]
439
+ };
467
440
  }
468
441
  }
469
442
  /** Optional curator-state service (evolution-state-json / storage-domain). */
@@ -966,8 +939,9 @@ var EvolutionCurator = class extends Service {
966
939
  if (!new RegExp(`^${name}-\\d{14}(-[0-9a-z]{1,6})?$`).test(entry)) continue;
967
940
  wasBundled = await this.io.exists(join(this.skills.root, ".archive", entry, markerEntryName("bundled")));
968
941
  if (wasBundled) {
969
- const archivedBody = await this.io.readText(join(this.skills.root, ".archive", entry, "SKILL.md")).catch(() => null);
970
- if (archivedBody !== null && parseFrontmatter(archivedBody)?.frontmatter.name !== name) wasBundled = false;
942
+ const archivedProbe = await probeText(this.io, join(this.skills.root, ".archive", entry, "SKILL.md"));
943
+ if (isUnknown(archivedProbe)) this.ctx.logger.warn(`evolution-curator: the archived body of "${entry}" could not be read (${archivedProbe.reason}); keeping the bundled marker as the signal`);
944
+ else if (isPresent(archivedProbe) && parseFrontmatter(archivedProbe.value)?.frontmatter.name !== name) wasBundled = false;
971
945
  }
972
946
  if (wasBundled) break;
973
947
  }
@@ -1137,24 +1111,22 @@ var EvolutionCurator = class extends Service {
1137
1111
  for (const name of entries.filter((entry) => entry.startsWith("curator-") && entry.endsWith(".json"))) try {
1138
1112
  const raw = await this.io.readText(join(reportsRoot, name));
1139
1113
  if (raw === null) continue;
1140
- const parsed = JSON.parse(raw);
1141
- let startedAt = typeof parsed.startedAt === "string" ? Date.parse(parsed.startedAt) : NaN;
1142
- if (!Number.isFinite(startedAt)) {
1143
- const probe = await probeMtime(this.io, join(reportsRoot, name));
1144
- if (isPresent(probe)) startedAt = probe.value;
1145
- }
1146
- if (!Number.isFinite(startedAt) && typeof parsed.at === "string") startedAt = Date.parse(parsed.at);
1147
- if (Number.isFinite(startedAt)) (name.startsWith("curator-error-") ? errors : real).push({
1114
+ const startedAt = await reportTime(this.io, join(reportsRoot, name), JSON.parse(raw));
1115
+ const probed = await probeMtime(this.io, join(reportsRoot, name));
1116
+ const mtime = isPresent(probed) ? probed.value : null;
1117
+ if (startedAt !== null) (name.startsWith("curator-error-") ? errors : real).push({
1148
1118
  name,
1149
- startedAt
1119
+ startedAt,
1120
+ mtime
1150
1121
  });
1151
1122
  else if (!unorderableWarned) {
1152
1123
  unorderableWarned = true;
1153
1124
  this.ctx.logger.warn(`evolution-curator: report "${name}" carries no usable timestamp and this backend has no mtime probe - it is kept outside the retention window`);
1154
1125
  }
1155
1126
  } catch {}
1156
- real.sort((a, b) => b.startedAt - a.startedAt);
1157
- errors.sort((a, b) => b.startedAt - a.startedAt);
1127
+ const newestFirst = (a, b) => b.startedAt - a.startedAt || (b.mtime ?? 0) - (a.mtime ?? 0) || b.name.localeCompare(a.name);
1128
+ real.sort(newestFirst);
1129
+ errors.sort(newestFirst);
1158
1130
  await this.pruneReportList(reportsRoot, real, keep);
1159
1131
  await this.pruneReportList(reportsRoot, errors, errorKeep);
1160
1132
  }
@@ -1181,19 +1153,26 @@ var EvolutionCurator = class extends Service {
1181
1153
  let latest = null;
1182
1154
  let probeWarned = false;
1183
1155
  for (const name of names) {
1184
- const probe = await probeMtime(this.io, join(reportsRoot, name));
1185
- if (isUnknown(probe)) {
1156
+ const path = join(reportsRoot, name);
1157
+ const raw = await this.io.readText(path);
1158
+ if (raw === null) continue;
1159
+ let parsed;
1160
+ try {
1161
+ parsed = JSON.parse(raw);
1162
+ } catch {
1163
+ continue;
1164
+ }
1165
+ const at = await reportTime(this.io, path, parsed);
1166
+ if (at === null) {
1186
1167
  if (!probeWarned) {
1187
1168
  probeWarned = true;
1188
- this.ctx.logger.warn(`evolution-curator: latestReport could not read a report mtime (${probe.reason}) - the affected report(s) are not eligible as the latest`);
1169
+ this.ctx.logger.warn("evolution-curator: latestReport found no usable report time (neither a declared startedAt/at nor an mtime probe) - the affected report(s) are not eligible as the latest");
1189
1170
  }
1190
1171
  continue;
1191
1172
  }
1192
- if (!isPresent(probe)) continue;
1193
- const mtime = probe.value;
1194
- if (latest === null || mtime > latest.mtime) latest = {
1173
+ if (latest === null || at > latest.at) latest = {
1195
1174
  name,
1196
- mtime
1175
+ at
1197
1176
  };
1198
1177
  }
1199
1178
  if (latest === null) return null;
@@ -29,7 +29,7 @@ export interface Config {
29
29
  /** The due-ness interval. The policy snapshot shadows it in every shipped
30
30
  * composition — configure `curatorIntervalHours` there; that name is the
31
31
  * canonical id. Deprecated alias (G0/S0.3): still readable, refused by
32
- * writes; removed 0.7.0. */
32
+ * writes; removal was planned for 0.7.0 and is deferred (the alias still ships — see the package README). */
33
33
  intervalHours?: number;
34
34
  staleAfterDays?: Volatile<number>;
35
35
  archiveAfterDays?: Volatile<number>;
@@ -205,6 +205,8 @@ export declare class EvolutionCurator extends Service {
205
205
  private readonly bootGraceSeconds;
206
206
  /** Keys whose supplied value had to be corrected; each warns once (G3.1). */
207
207
  private readonly clampedKeys;
208
+ /** The clamp warning sink — the LOGIC lives in core's `clampOnce` (S2.9/T3-10), the prefix here. */
209
+ private readonly clampWarn;
208
210
  private lastRun;
209
211
  private timer;
210
212
  /** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
@@ -232,16 +234,6 @@ export declare class EvolutionCurator extends Service {
232
234
  * yielding instance's log line names a concrete holder. */
233
235
  private readonly instanceOwner;
234
236
  constructor(ctx: Context, config?: Config);
235
- /**
236
- * Clamp one supplied numeric value, warning once per key (G3.1).
237
- * @param key - the config key, for the warning.
238
- * @param value - the value as supplied by the deployment or the user.
239
- * @param fallback - the package default a corrected value falls back to.
240
- * @param min - the smallest value the engine can act on.
241
- * @param max - the largest value the carrier can hold, when bounded.
242
- * @returns the value the engine may use.
243
- */
244
- private clampOnce;
245
237
  /**
246
238
  * One live numeric row field at USE time (G1 §8.3): the row config carries the
247
239
  * platform-resolved layers (schema default < bundle row < profile override) and the
@@ -272,17 +264,6 @@ export declare class EvolutionCurator extends Service {
272
264
  * @returns the row layer of the curator's behaviour knobs.
273
265
  */
274
266
  private rowSettings;
275
- /**
276
- * The keys the user set in this row's namespace (G1 §8.4-A).
277
- *
278
- * This is the ONLY remaining read of the settings user layer, and it reads KEY
279
- * NAMES, never values: the platform resolves the user layer into the row config,
280
- * so a value is needed from `config` — but the precedence rule (user > policy)
281
- * needs to know WHICH keys the user set, or a deployment policy would silently
282
- * override the user.
283
- * @returns the keys present in this row's user layer.
284
- */
285
- private userSetKeys;
286
267
  /** The policy-snapshot carrier for this group (unmounted service = no policy). */
287
268
  private policyFields;
288
269
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-curator",
3
3
  "description": "Deterministic skill lifecycle and recovery (community build)",
4
- "version": "0.17.1",
4
+ "version": "0.18.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -26,21 +26,21 @@
26
26
  ],
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-core": "^0.17.1"
29
+ "@deepseek-ai/schemastery": "^3.18.4",
30
+ "@lmzhen/dsh-evolution-core": "^0.18.1"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
34
  "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
35
35
  "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
36
- "@lmzhen/dsh-evolution-io": "^0.17.1",
37
- "@lmzhen/dsh-evolution-state": "^0.17.1"
36
+ "@lmzhen/dsh-evolution-io": "^0.18.1",
37
+ "@lmzhen/dsh-evolution-state": "^0.18.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
41
41
  "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
42
- "@lmzhen/dsh-evolution-core": "^0.17.1",
43
- "@lmzhen/dsh-evolution-io": "^0.17.1",
44
- "@lmzhen/dsh-evolution-state": "^0.17.1"
42
+ "@lmzhen/dsh-evolution-core": "^0.18.1",
43
+ "@lmzhen/dsh-evolution-io": "^0.18.1",
44
+ "@lmzhen/dsh-evolution-state": "^0.18.1"
45
45
  }
46
46
  }