@lmzhen/dsh-evolution-curator 0.23.0 → 0.25.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
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import z from "@deepseek-ai/schemastery";
6
6
  import { preferProvider } from "@lmzhen/dsh-evolution-io";
7
- 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_REVIEW_TIMEOUT_MS, 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, liveBool, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, paramRowId, parseCuratorNominations, parseFrontmatter, pickWithPolicy, policyStageLimits, probeList, probeText, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, reportTime, reportsSweepLockTarget, sessionLastEventTime, sweepReports, transactIo, unwrapLive, updateSuppressedNames, usageObserved, userSetKeys, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
7
+ 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_REVIEW_TIMEOUT_MS, 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, lintSkill, liveBool, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, paramRowId, parseCuratorNominations, parseFrontmatter, pickWithPolicy, policyStageLimits, probeList, probeText, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, reportTime, reportsSweepLockTarget, sessionLastEventTime, sweepReports, transactIo, unwrapLive, updateSuppressedNames, usageObserved, userSetKeys, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
8
8
  //#region lib/types/index.js
9
9
  /**
10
10
  * Deterministic skill lifecycle curator with interval gate and archive.
@@ -33,6 +33,24 @@ function gateConsolidations(consolidations, gates) {
33
33
  const gateSet = gates instanceof EvolutionGateSet ? gates : new EvolutionGateSet(gates);
34
34
  return consolidations.filter((n) => !gateSet.isBlocked(n.from) && !gateSet.isBlocked(n.into));
35
35
  }
36
+ /**
37
+ * G5 (in-use protection): which skills are POINTED AT by others, and by whom.
38
+ *
39
+ * Pure: derived from the bodies already in hand, using `relatedSkillNames` — the single reader of
40
+ * that frontmatter field — so this map can never disagree with the catalog or the audit. The value
41
+ * is a Set of SOURCES, not a boolean, because a refusal has to be able to name the referrers.
42
+ * @param contents - skill name to its whole SKILL.md text.
43
+ * @returns target name to the set of skills naming it; targets nobody names are absent.
44
+ */
45
+ function referencedSkillNames(contents) {
46
+ const referencedBy = /* @__PURE__ */ new Map();
47
+ for (const [owner, text] of contents) for (const target of relatedSkillNames(text, owner)) {
48
+ const sources = referencedBy.get(target) ?? /* @__PURE__ */ new Set();
49
+ sources.add(owner);
50
+ referencedBy.set(target, sources);
51
+ }
52
+ return referencedBy;
53
+ }
36
54
  var EvolutionCurator = class extends Service {
37
55
  static inject = ["evolutionIo"];
38
56
  static Config = z.object({
@@ -609,7 +627,7 @@ var EvolutionCurator = class extends Service {
609
627
  instanceHeldRefusal(operation) {
610
628
  return {
611
629
  ok: false,
612
- message: `instance-held: this curator row yielded the per-home claim to another curator row of this process — ${operation} is refused alongside run() so the holder row stays the only control-plane writer (run it on the holder row)`
630
+ message: `instance-held:本 curator 行已把每 home 的占用权让给本进程的另一 curator 行。为保证持有行是唯一的控制面写入方,${operation} 与 run() 一并被拒绝(请在持有它的那一行上运行)。`
613
631
  };
614
632
  }
615
633
  async runCore(options = {}) {
@@ -691,8 +709,9 @@ var EvolutionCurator = class extends Service {
691
709
  const text = await this.skills.read(name);
692
710
  if (text) contents.set(name, text);
693
711
  }
712
+ const referencedBy = referencedSkillNames(contents);
694
713
  const protectedNames = await this.protectedNameMap();
695
- const dedupMembers = [...new Set(computeDedupGroups({ contents }).groups.filter((group) => group.length >= 2).flat())].filter((name) => !protectedNames.has(name));
714
+ const dedupMembers = [...new Set(computeDedupGroups({ contents }).groups.filter((group) => group.length >= 2).flat())].filter((name) => !protectedNames.has(name) && !referencedBy.has(name));
696
715
  await this.scoreTree(usage, treeNames, contents);
697
716
  const result = computeLifecycleTransitions(usage, {
698
717
  staleAfterDays: lifecycle.staleAfterDays,
@@ -968,7 +987,7 @@ var EvolutionCurator = class extends Service {
968
987
  continue;
969
988
  }
970
989
  const archived = await this.skills.archive(name, {
971
- reason: "Lifecycle: reached archive threshold",
990
+ reason: "生命周期:已达归档阈值",
972
991
  allowBundled: this.pruneBuiltins
973
992
  });
974
993
  if (!archived.ok) {
@@ -990,7 +1009,7 @@ var EvolutionCurator = class extends Service {
990
1009
  archivedSkills.push({
991
1010
  name,
992
1011
  path: archived.path ?? "",
993
- reason: "Lifecycle: reached archive threshold"
1012
+ reason: "生命周期:已达归档阈值"
994
1013
  });
995
1014
  alreadyArchived.add(name);
996
1015
  if (bundledNames.has(name)) {
@@ -1035,7 +1054,7 @@ var EvolutionCurator = class extends Service {
1035
1054
  });
1036
1055
  archivedSkills.push({
1037
1056
  name: nomination.from,
1038
- reason: `Consolidated into ${nomination.into} (exact archive path: the evolution/skill-mutated event, archivedPath)`
1057
+ reason: `已合并进 ${nomination.into}(精确归档路径见 evolution/skill-mutated 事件里的 archivedPath)`
1039
1058
  });
1040
1059
  }
1041
1060
  if (suppressedChanged) try {
@@ -1219,10 +1238,47 @@ var EvolutionCurator = class extends Service {
1219
1238
  patchCount: record.patch_count,
1220
1239
  readCount: record.view_count
1221
1240
  } : void 0);
1222
- if (assessment && assessment.verdict !== "healthy") rows.push({
1241
+ if (assessment !== null) {
1242
+ rows.push({
1243
+ name: summary.name,
1244
+ verdict: assessment.verdict,
1245
+ dims: assessment.dims,
1246
+ findings: assessment.findings
1247
+ });
1248
+ continue;
1249
+ }
1250
+ const findings = lintSkill({
1251
+ skillName: summary.name,
1252
+ dims: {
1253
+ bodyChars: null,
1254
+ bodyTokens: null,
1255
+ stampDensityPerKb: null,
1256
+ stampHits: null,
1257
+ supportGroups: 0,
1258
+ supportFiles: null,
1259
+ churnPatches: null,
1260
+ churnReads: null
1261
+ },
1262
+ softLineChars: thresholds.softBodyChars,
1263
+ overSoftLine: false,
1264
+ overTwiceSoftLine: false,
1265
+ stampLimitPerKb: thresholds.stampDensityPerKb,
1266
+ churnMinPatches: thresholds.churnMinPatches,
1267
+ readable: false
1268
+ });
1269
+ rows.push({
1223
1270
  name: summary.name,
1224
- verdict: assessment.verdict,
1225
- reasons: assessment.reasons
1271
+ verdict: "warn",
1272
+ dims: {
1273
+ bodyChars: null,
1274
+ bodyTokens: null,
1275
+ stampDensityPerKb: null,
1276
+ supportGroups: 0,
1277
+ supportFiles: null,
1278
+ churnPatches: null,
1279
+ churnReads: null
1280
+ },
1281
+ findings
1226
1282
  });
1227
1283
  }
1228
1284
  return rows;
@@ -1262,7 +1318,7 @@ var EvolutionCurator = class extends Service {
1262
1318
  const blocked = [...new Set([target, ...sources])].filter((name) => gates.isBlocked(name));
1263
1319
  if (blocked.length > 0) return {
1264
1320
  ok: false,
1265
- message: `Skill(s) protected from consolidation (excluded / referenced / suppressed / protected builtin): ${blocked.join(", ")}`
1321
+ message: `以下技能受保护、无法合并(被排除/被引用/被抑制/受保护的内置):${blocked.join(", ")}`
1266
1322
  };
1267
1323
  await this.snapshotFull("pre-consolidate");
1268
1324
  const result = await this.skills.consolidate(target, sources);
@@ -1360,46 +1416,46 @@ var EvolutionCurator = class extends Service {
1360
1416
  const versions = await this.skills.listVersions(name);
1361
1417
  if (versions.length === 0) return {
1362
1418
  ok: false,
1363
- message: `No content versions are recorded for "${name}" yet — nothing to undo.`
1419
+ message: `技能「${name}」还没有记录内容版本 —— 没有可撤销的。`
1364
1420
  };
1365
1421
  const live = await this.skills.read(name);
1366
1422
  if (live === null) return {
1367
1423
  ok: false,
1368
- message: `"${name}" is not in the active tree — undo restores CONTENT of a live skill; use '/evolution skill restore ${name}' for an archived one.`
1424
+ message: `「${name}」不在活动目录树里 —— 撤销恢复的是存活技能的正文;若它已归档,请用 '/evolution skill restore ${name}'。`
1369
1425
  };
1370
1426
  const liveHash = contentHash(live);
1371
1427
  const target = v ?? this.previousVersionOf(versions, liveHash);
1372
1428
  if (target === void 0) return {
1373
1429
  ok: false,
1374
- message: `"${name}" is already at its recorded content — no earlier version to undo to.`
1430
+ message: `「${name}」已处于其记录的内容 —— 没有更早的版本可撤销。`
1375
1431
  };
1376
1432
  if (!versions.some((candidate) => candidate.v === target)) {
1377
1433
  const listed = versions.map((candidate) => `v${candidate.v}`).join(", ");
1378
1434
  return {
1379
1435
  ok: false,
1380
- message: `Version v${target} of "${name}" is not in the recorded history (recorded: ${listed === "" ? "none" : listed}).`
1436
+ message: `技能「${name}」的版本 v${target} 不在已记录的历史里(已记录:${listed === "" ? "无" : listed})。`
1381
1437
  };
1382
1438
  }
1383
1439
  const entry = versions.find((candidate) => candidate.v === target);
1384
1440
  if (entry !== void 0 && entryTarget(entry, versions) === "support") return {
1385
1441
  ok: false,
1386
- message: `Version v${target} of "${name}" ${entry.path === void 0 ? `was written by '${entry.action}' and` : `was written by '${entry.action}' to '${entry.path}' and`} holds a SUPPORT FILE's bytes, not this skill's SKILL.md — undo restores the body only. The recorded bytes stay in the history store; copy the blob back over that file to restore them.`
1442
+ message: `技能「${name}」的版本 v${target} ${entry.path === void 0 ? `由 ${entry.action} 写入,并且` : `由 ${entry.action} 写入到 ${entry.path},并且`} 存的是支撑文件的字节,而不是该技能的 SKILL.md —— 撤销只会恢复正文。 已记录的字节仍留在历史存储里;要恢复它们,请把该 blob 复制回那个文件。`
1387
1443
  };
1388
1444
  const content = await this.skills.readVersion(name, target);
1389
1445
  if (content === null) return {
1390
1446
  ok: false,
1391
- message: `Version v${target} of "${name}" is listed in the index but its stored content could not be read — refusing rather than writing a guess.`
1447
+ message: `技能「${name}」的版本 v${target} 虽在索引里,但它的存储内容读不出来 —— 拒绝瞎猜、不做写入。`
1392
1448
  };
1393
1449
  const anchor = { sha256: liveHash };
1394
1450
  const result = await this.skills.update(name, content, "foreground", anchor);
1395
1451
  if (!result.ok) return result;
1396
1452
  if (result.noop === true) return {
1397
1453
  ok: true,
1398
- message: `"${name}" already holds the content of v${target} — nothing was written and no new version was recorded.`
1454
+ message: `「${name}」已持有 v${target} 的内容 —— 未写入任何内容,也没有记录新版本。`
1399
1455
  };
1400
1456
  return {
1401
1457
  ok: true,
1402
- message: `${result.message} (undone to v${target} — content only: markers, usage counts and curation state are unchanged, and this undo is itself a new version.)`
1458
+ message: `${result.message} (已撤销到 v${target} —— 仅内容:标记、使用计数与策展状态不变,且这次撤销本身就是一个新版本。)`
1403
1459
  };
1404
1460
  }
1405
1461
  /** The content the live bytes replaced: the CHAIN first, the recorded order as the fallback.
@@ -1426,4 +1482,4 @@ var EvolutionCurator = class extends Service {
1426
1482
  }
1427
1483
  };
1428
1484
  //#endregion
1429
- export { EvolutionCurator, EvolutionCurator as default, gateConsolidations, validateCuratorSettings };
1485
+ export { EvolutionCurator, EvolutionCurator as default, gateConsolidations, referencedSkillNames, validateCuratorSettings };
@@ -148,6 +148,16 @@ export declare function gateConsolidations(consolidations: CuratorConsolidation[
148
148
  referenced?: ReadonlySet<string>;
149
149
  suppressed?: ReadonlySet<string>;
150
150
  }): CuratorConsolidation[];
151
+ /**
152
+ * G5 (in-use protection): which skills are POINTED AT by others, and by whom.
153
+ *
154
+ * Pure: derived from the bodies already in hand, using `relatedSkillNames` — the single reader of
155
+ * that frontmatter field — so this map can never disagree with the catalog or the audit. The value
156
+ * is a Set of SOURCES, not a boolean, because a refusal has to be able to name the referrers.
157
+ * @param contents - skill name to its whole SKILL.md text.
158
+ * @returns target name to the set of skills naming it; targets nobody names are absent.
159
+ */
160
+ export declare function referencedSkillNames(contents: ReadonlyMap<string, string>): Map<string, Set<string>>;
151
161
  export declare class EvolutionCurator extends Service {
152
162
  static inject: string[];
153
163
  static Config: z<Schemastery.ObjectS<NoInfer<{
@@ -495,7 +505,21 @@ export declare class EvolutionCurator extends Service {
495
505
  healthView(): Promise<Array<{
496
506
  name: string;
497
507
  verdict: SkillHealthVerdict;
498
- reasons: string[];
508
+ dims: {
509
+ bodyChars: number | null;
510
+ bodyTokens: number | null;
511
+ stampDensityPerKb: number | null;
512
+ supportGroups: number;
513
+ supportFiles: number | null;
514
+ churnPatches: number | null;
515
+ churnReads: number | null;
516
+ };
517
+ findings: Array<{
518
+ severity: 'warn' | 'error';
519
+ rule: string;
520
+ message: string;
521
+ count: number;
522
+ }>;
499
523
  }>>;
500
524
  /** Whether the library has ANY observed read evidence (C observation-window gate for churn signals). */
501
525
  usageObserved(): Promise<boolean>;
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.23.0",
4
+ "version": "0.25.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,20 +27,20 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.4",
30
- "@lmzhen/dsh-evolution-core": "^0.23.0"
30
+ "@lmzhen/dsh-evolution-core": "^0.25.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.23.0",
37
- "@lmzhen/dsh-evolution-state": "^0.23.0"
36
+ "@lmzhen/dsh-evolution-io": "^0.25.0",
37
+ "@lmzhen/dsh-evolution-state": "^0.25.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.23.0",
43
- "@lmzhen/dsh-evolution-io": "^0.23.0",
44
- "@lmzhen/dsh-evolution-state": "^0.23.0"
42
+ "@lmzhen/dsh-evolution-core": "^0.25.0",
43
+ "@lmzhen/dsh-evolution-io": "^0.25.0",
44
+ "@lmzhen/dsh-evolution-state": "^0.25.0"
45
45
  }
46
46
  }