@lmzhen/dsh-evolution-curator 0.9.0 → 0.11.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/README.md CHANGED
@@ -19,6 +19,8 @@ Deterministic skill lifecycle and recovery
19
19
 
20
20
  - `archive` never deletes: skills move to `.archive/` with a `.archive-reason` marker.
21
21
  - `restore(name)` (service) / `/evolution skill restore <name>` brings one archived skill back to the active root and resets its usage state.
22
+ - `history(name)` (service) / `/evolution skill history <name>` lists one skill's content versions (number, time, action, characters, hash prefix) from the content history every write path maintains (`evolution-core/src/skill-history.ts`). An index that cannot be read, carries a foreign field layout, or was written by a NEWER reader all read as "no versions" (the module's never-guess posture) — the next successful write re-seeds the index with a baseline entry, so the record heals instead of staying invisible.
23
+ - `undo(name, v?)` (service) / `/evolution skill undo <name> [--to v<N>]` restores one version's CONTENT through an ordinary anchored write: an omitted `v` means the newest version whose content differs from the live file, the anchor is the live hash (a concurrent writer is refused rather than overwritten, and a version the index lists but whose blob cannot be read is refused rather than guessed at), and the rollback itself becomes a new version. Content only: the pin marker, usage counters and curation state are not versions and do not move, which the result text says. A skill with no recorded versions, a target that is not in the active tree (use `skill restore` for an archived one), and a session that cannot replay the write each get their own refusal.
22
24
  - `consolidate(target, sources)` (service) / `/evolution consolidate` merges source bodies into the target, archives the sources with an absorbed-into marker, and folds their usage records into `archived` state. Both operations snapshot the full state first (`pre-consolidate` / `pre-restore`).
23
25
  - `restoreSnapshot` (service) / `/evolution restore` rolls the state back to the latest snapshot: active tree, usage/suppression sidecars, `.archive/` and the curator state carried in the snapshot (`curator-state.json`), so the interval gate does not immediately re-fire after a rollback. Skills that were skipped at snapshot time (a live writer held their lock; recorded in the manifest's `skipped` list) are NOT restored — the restore result names them, and `.backups` may hold a copy. The restore itself is undoable — the pre-rollback safety snapshot preserves the current tree plus its state.
24
26
 
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, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, installParamSection, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, paramNamespace, parseCuratorNominations, parseFrontmatter, policyStageLimits, probeList, probeMtime, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, 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, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, emptyRecord, evolutionHome, evolutionIoAdapter, foldCuratorFields, installParamSection, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, paramNamespace, parseCuratorNominations, parseFrontmatter, policyStageLimits, probeList, probeMtime, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, transactIo, updateSuppressedNames, usageObserved, versionTarget } 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.
@@ -1320,6 +1320,105 @@ var EvolutionCurator = class extends Service {
1320
1320
  }
1321
1321
  return result;
1322
1322
  }
1323
+ /**
1324
+ * One skill's content versions, oldest first (skill-history.ts, batch A).
1325
+ *
1326
+ * READ-ONLY on purpose: no control-plane mutex and no instance claim. A human asking "which
1327
+ * versions exist?" must not be blocked by a running pass, and this cannot change anything.
1328
+ * @param name - the skill's name.
1329
+ * @returns the versions; a skill with no history (or an index that cannot be read) answers [].
1330
+ */
1331
+ async history(name) {
1332
+ return await this.skills.listVersions(name);
1333
+ }
1334
+ /**
1335
+ * Control-plane undo: put one skill's CONTENT back to a recorded version.
1336
+ *
1337
+ * The undo is an ordinary anchored write (design I5): it goes through `update`, so it earns its
1338
+ * own version, joins the mutation ledger, and is refused if the live content changed since the
1339
+ * caller read it — there is no second write channel to bypass. Only content moves: markers
1340
+ * (`.pinned`/`.hermes-managed`), usage counters and curation state are deliberately untouched,
1341
+ * which the result message says so nobody reads this as a time machine.
1342
+ * @param name - the skill's name.
1343
+ * @param v - the version to restore; omitted means "the newest version that differs from the live
1344
+ * content", i.e. the state before the latest change.
1345
+ * @returns the write result (`ok:false` with the reason when there is nothing to undo).
1346
+ */
1347
+ async undo(name, v) {
1348
+ const release = await this.acquireMutex();
1349
+ try {
1350
+ if (!this.holdsInstance) return this.instanceHeldRefusal("undo");
1351
+ return await this.undoMutate(name, v);
1352
+ } finally {
1353
+ release();
1354
+ }
1355
+ }
1356
+ async undoMutate(name, v) {
1357
+ const versions = await this.skills.listVersions(name);
1358
+ if (versions.length === 0) return {
1359
+ ok: false,
1360
+ message: `No content versions are recorded for "${name}" yet — nothing to undo.`
1361
+ };
1362
+ const live = await this.skills.read(name);
1363
+ if (live === null) return {
1364
+ ok: false,
1365
+ message: `"${name}" is not in the active tree — undo restores CONTENT of a live skill; use '/evolution skill restore ${name}' for an archived one.`
1366
+ };
1367
+ const liveHash = contentHash(live);
1368
+ const target = v ?? this.previousVersionOf(versions, liveHash);
1369
+ if (target === void 0) return {
1370
+ ok: false,
1371
+ message: `"${name}" is already at its recorded content — no earlier version to undo to.`
1372
+ };
1373
+ if (!versions.some((candidate) => candidate.v === target)) {
1374
+ const listed = versions.map((candidate) => `v${candidate.v}`).join(", ");
1375
+ return {
1376
+ ok: false,
1377
+ message: `Version v${target} of "${name}" is not in the recorded history (recorded: ${listed === "" ? "none" : listed}).`
1378
+ };
1379
+ }
1380
+ const entry = versions.find((candidate) => candidate.v === target);
1381
+ if (entry !== void 0 && versionTarget(entry.action) === "support") return {
1382
+ ok: false,
1383
+ message: `Version v${target} of "${name}" was written by '${entry.action}' 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.`
1384
+ };
1385
+ const content = await this.skills.readVersion(name, target);
1386
+ if (content === null) return {
1387
+ ok: false,
1388
+ 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.`
1389
+ };
1390
+ const anchor = { sha256: liveHash };
1391
+ const result = await this.skills.update(name, content, "foreground", anchor);
1392
+ if (!result.ok) return result;
1393
+ if (result.noop === true) return {
1394
+ ok: true,
1395
+ message: `"${name}" already holds the content of v${target} — nothing was written and no new version was recorded.`
1396
+ };
1397
+ return {
1398
+ ok: true,
1399
+ 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.)`
1400
+ };
1401
+ }
1402
+ /** The content the live bytes replaced: the CHAIN first, the recorded order as the fallback.
1403
+ *
1404
+ * Every entry written from 0.10.0 on carries `beforeHash`, read under the content lock, so the link
1405
+ * survives two writers of one skill appending their versions out of order. The tail scan stays for
1406
+ * entries written before the link existed and for a live body no version recorded (a hand-edited
1407
+ * tree): the newest version whose content differs from the live bytes.
1408
+ *
1409
+ * Support-file versions are skipped in both branches: undo restores the skill BODY, and a version
1410
+ * holding a support file's bytes is not a state the body was ever in (0.10.1). */
1411
+ previousVersionOf(versions, liveHash) {
1412
+ const live = [...versions].reverse().find((entry) => entry.hash === liveHash);
1413
+ if (live?.beforeHash !== void 0) {
1414
+ const predecessor = [...versions].reverse().find((entry) => entry.hash === live.beforeHash);
1415
+ if (predecessor !== void 0 && versionTarget(predecessor.action) !== "support") return predecessor.v;
1416
+ }
1417
+ for (let index = versions.length - 1; index >= 0; index -= 1) {
1418
+ const entry = versions[index];
1419
+ if (entry !== void 0 && entry.hash !== liveHash && versionTarget(entry.action) !== "support") return entry.v;
1420
+ }
1421
+ }
1323
1422
  };
1324
1423
  /**
1325
1424
  * v43 FLOW2-1: the per-home lock target for the report retention sweep.
@@ -7,6 +7,7 @@ import z from '@deepseek-ai/schemastery';
7
7
  import type Schema from '@deepseek-ai/schemastery';
8
8
  import { EvolutionGateSet, type SkillLibrary } from '@lmzhen/dsh-evolution-core';
9
9
  import { type CuratorConsolidation, type CuratorNominations, type CuratorRunReport, type ScopeView, type SkillActionResult, type SkillHealthVerdict } from '@lmzhen/dsh-evolution-core';
10
+ import type { SkillVersion } from '@lmzhen/dsh-evolution-core';
10
11
  import type { CuratorStateRecord } from '@lmzhen/dsh-evolution-state';
11
12
  declare module '@deepseek-ai/cordis' {
12
13
  interface Context {
@@ -416,6 +417,40 @@ export declare class EvolutionCurator extends Service {
416
417
  */
417
418
  restore(name: string): Promise<SkillActionResult>;
418
419
  private restoreMutate;
420
+ /**
421
+ * One skill's content versions, oldest first (skill-history.ts, batch A).
422
+ *
423
+ * READ-ONLY on purpose: no control-plane mutex and no instance claim. A human asking "which
424
+ * versions exist?" must not be blocked by a running pass, and this cannot change anything.
425
+ * @param name - the skill's name.
426
+ * @returns the versions; a skill with no history (or an index that cannot be read) answers [].
427
+ */
428
+ history(name: string): Promise<SkillVersion[]>;
429
+ /**
430
+ * Control-plane undo: put one skill's CONTENT back to a recorded version.
431
+ *
432
+ * The undo is an ordinary anchored write (design I5): it goes through `update`, so it earns its
433
+ * own version, joins the mutation ledger, and is refused if the live content changed since the
434
+ * caller read it — there is no second write channel to bypass. Only content moves: markers
435
+ * (`.pinned`/`.hermes-managed`), usage counters and curation state are deliberately untouched,
436
+ * which the result message says so nobody reads this as a time machine.
437
+ * @param name - the skill's name.
438
+ * @param v - the version to restore; omitted means "the newest version that differs from the live
439
+ * content", i.e. the state before the latest change.
440
+ * @returns the write result (`ok:false` with the reason when there is nothing to undo).
441
+ */
442
+ undo(name: string, v?: number): Promise<SkillActionResult>;
443
+ private undoMutate;
444
+ /** The content the live bytes replaced: the CHAIN first, the recorded order as the fallback.
445
+ *
446
+ * Every entry written from 0.10.0 on carries `beforeHash`, read under the content lock, so the link
447
+ * survives two writers of one skill appending their versions out of order. The tail scan stays for
448
+ * entries written before the link existed and for a live body no version recorded (a hand-edited
449
+ * tree): the newest version whose content differs from the live bytes.
450
+ *
451
+ * Support-file versions are skipped in both branches: undo restores the skill BODY, and a version
452
+ * holding a support file's bytes is not a state the body was ever in (0.10.1). */
453
+ private previousVersionOf;
419
454
  }
420
455
  export default EvolutionCurator;
421
456
  //# sourceMappingURL=index.d.ts.map
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.9.0",
4
+ "version": "0.11.1",
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.1",
30
- "@lmzhen/dsh-evolution-core": "^0.9.0"
30
+ "@lmzhen/dsh-evolution-core": "^0.11.1"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
34
  "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
35
35
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
36
- "@lmzhen/dsh-evolution-io": "^0.9.0",
37
- "@lmzhen/dsh-evolution-state": "^0.9.0"
36
+ "@lmzhen/dsh-evolution-io": "^0.11.1",
37
+ "@lmzhen/dsh-evolution-state": "^0.11.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
41
41
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
42
- "@lmzhen/dsh-evolution-core": "^0.9.0",
43
- "@lmzhen/dsh-evolution-io": "^0.9.0",
44
- "@lmzhen/dsh-evolution-state": "^0.9.0"
42
+ "@lmzhen/dsh-evolution-core": "^0.11.1",
43
+ "@lmzhen/dsh-evolution-io": "^0.11.1",
44
+ "@lmzhen/dsh-evolution-state": "^0.11.1"
45
45
  }
46
46
  }