@lmzhen/dsh-evolution-curator 0.17.1 → 0.18.0

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 } : {
@@ -966,8 +934,9 @@ var EvolutionCurator = class extends Service {
966
934
  if (!new RegExp(`^${name}-\\d{14}(-[0-9a-z]{1,6})?$`).test(entry)) continue;
967
935
  wasBundled = await this.io.exists(join(this.skills.root, ".archive", entry, markerEntryName("bundled")));
968
936
  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;
937
+ const archivedProbe = await probeText(this.io, join(this.skills.root, ".archive", entry, "SKILL.md"));
938
+ 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`);
939
+ else if (isPresent(archivedProbe) && parseFrontmatter(archivedProbe.value)?.frontmatter.name !== name) wasBundled = false;
971
940
  }
972
941
  if (wasBundled) break;
973
942
  }
@@ -1137,24 +1106,22 @@ var EvolutionCurator = class extends Service {
1137
1106
  for (const name of entries.filter((entry) => entry.startsWith("curator-") && entry.endsWith(".json"))) try {
1138
1107
  const raw = await this.io.readText(join(reportsRoot, name));
1139
1108
  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({
1109
+ const startedAt = await reportTime(this.io, join(reportsRoot, name), JSON.parse(raw));
1110
+ const probed = await probeMtime(this.io, join(reportsRoot, name));
1111
+ const mtime = isPresent(probed) ? probed.value : null;
1112
+ if (startedAt !== null) (name.startsWith("curator-error-") ? errors : real).push({
1148
1113
  name,
1149
- startedAt
1114
+ startedAt,
1115
+ mtime
1150
1116
  });
1151
1117
  else if (!unorderableWarned) {
1152
1118
  unorderableWarned = true;
1153
1119
  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
1120
  }
1155
1121
  } catch {}
1156
- real.sort((a, b) => b.startedAt - a.startedAt);
1157
- errors.sort((a, b) => b.startedAt - a.startedAt);
1122
+ const newestFirst = (a, b) => b.startedAt - a.startedAt || (b.mtime ?? 0) - (a.mtime ?? 0) || b.name.localeCompare(a.name);
1123
+ real.sort(newestFirst);
1124
+ errors.sort(newestFirst);
1158
1125
  await this.pruneReportList(reportsRoot, real, keep);
1159
1126
  await this.pruneReportList(reportsRoot, errors, errorKeep);
1160
1127
  }
@@ -1181,19 +1148,26 @@ var EvolutionCurator = class extends Service {
1181
1148
  let latest = null;
1182
1149
  let probeWarned = false;
1183
1150
  for (const name of names) {
1184
- const probe = await probeMtime(this.io, join(reportsRoot, name));
1185
- if (isUnknown(probe)) {
1151
+ const path = join(reportsRoot, name);
1152
+ const raw = await this.io.readText(path);
1153
+ if (raw === null) continue;
1154
+ let parsed;
1155
+ try {
1156
+ parsed = JSON.parse(raw);
1157
+ } catch {
1158
+ continue;
1159
+ }
1160
+ const at = await reportTime(this.io, path, parsed);
1161
+ if (at === null) {
1186
1162
  if (!probeWarned) {
1187
1163
  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`);
1164
+ 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
1165
  }
1190
1166
  continue;
1191
1167
  }
1192
- if (!isPresent(probe)) continue;
1193
- const mtime = probe.value;
1194
- if (latest === null || mtime > latest.mtime) latest = {
1168
+ if (latest === null || at > latest.at) latest = {
1195
1169
  name,
1196
- mtime
1170
+ at
1197
1171
  };
1198
1172
  }
1199
1173
  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.0",
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.0"
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.0",
37
+ "@lmzhen/dsh-evolution-state": "^0.18.0"
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.0",
43
+ "@lmzhen/dsh-evolution-io": "^0.18.0",
44
+ "@lmzhen/dsh-evolution-state": "^0.18.0"
45
45
  }
46
46
  }