@lmzhen/dsh-evolution-maintenance 0.3.67 → 0.3.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.
@@ -1,4 +1,4 @@
1
- import { HEALTH_STAMP_RE, MIN_STAMP_BODY_CHARS, computeDedupGroups, computePrefixClusters, duplicateHeadings, frontmatterYamlUnsafeValues, missingSupportPointers, narrowNameMatches, overlongLines, parseFrontmatter, usageObserved } from "@lmzhen/dsh-evolution-core";
1
+ import { HEALTH_STAMP_RE, MIN_STAMP_BODY_CHARS, computeDedupGroups, computePrefixClusters, duplicateHeadings, frontmatterCatalogInvalid, missingSupportPointers, narrowNameMatches, overlongLines, parseFrontmatter, usageObserved } from "@lmzhen/dsh-evolution-core";
2
2
  //#region lib/types/drift-scan.js
3
3
  /**
4
4
  * Skill-library snapshot assembly for drift scanning.
@@ -24,7 +24,10 @@ async function snapshotFromLibrary(library, options = {}) {
24
24
  options.onReadError?.(entry.name, error);
25
25
  continue;
26
26
  }
27
- if (body === void 0 || body === null) continue;
27
+ if (body === void 0 || body === null) {
28
+ options.onReadError?.(entry.name, /* @__PURE__ */ new Error("no readable SKILL.md (missing file, or the directory name is not a valid skill name)"));
29
+ continue;
30
+ }
28
31
  snapshots.push({
29
32
  name: entry.name,
30
33
  body,
@@ -141,7 +144,7 @@ async function buildEnrichment(ctx, library) {
141
144
  const body = await library.read(entry.name);
142
145
  if (body === void 0 || body === null) continue;
143
146
  const parsed = parseFrontmatter(body);
144
- if (frontmatterYamlUnsafeValues(body).length > 0) catalogInvalid.set(entry.name, true);
147
+ if (frontmatterCatalogInvalid(body)) catalogInvalid.set(entry.name, true);
145
148
  const description = parsed?.frontmatter.description;
146
149
  if (typeof description === "string" && description.trim().length > 0) descriptions.set(entry.name, description);
147
150
  const files = await library.listSupportFiles(entry.name);
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { i as snapshotFromLibrary, n as PROBE_SIGNALS, r as computeProbe, t as buildEnrichment } from "./enrichment-CYpvRfH_.js";
2
- import { AUTHORING_DESCRIPTION_BAR, DEFAULT_HEALTH_THRESHOLDS, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MIN_STAMP_BODY_CHARS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, computeDriftSignals, findDriftSignal, redactSecrets, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
1
+ import { i as snapshotFromLibrary, n as PROBE_SIGNALS, r as computeProbe, t as buildEnrichment } from "./enrichment-2NEuOAZU.js";
2
+ import { AUTHORING_DESCRIPTION_BAR, DEFAULT_CURATOR_MODEL, DEFAULT_HEALTH_THRESHOLDS, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, LOW_QUALITY_THRESHOLD, MAINTAIN_OUTPUT_INSTRUCTION, MAINTAIN_PROMPT, MAX_TIMER_DELAY_MS, MIN_STAMP_BODY_CHARS, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, computeDriftSignals, findDriftSignal, redactSecrets, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  //#region lib/types/render-facts.js
5
5
  /**
@@ -49,6 +49,16 @@ function renderSignal(signal, redact) {
49
49
  * and the quality_low=unknown global imposition. Never judges whether a
50
50
  * recommendation is right — only whether it is well-formed and traceable.
51
51
  */
52
+ /**
53
+ * V27 D-3: the clause ids the maintain template ITSELF enumerates — the `- A1 …`
54
+ * bullets of its §5 rule catalogue. `rule` is checked against this set (parsed
55
+ * from the shipped template at module load, so a template edit that adds or
56
+ * renames a clause moves the set with it) instead of the former bare
57
+ * non-empty-string check, which let a plan cite a clause number that does not
58
+ * exist and still pass mechanical validation. `self-consistency.spec.ts` pins
59
+ * the parsed set so a template rewording cannot silently empty it.
60
+ */
61
+ const MAINTAIN_RULE_IDS = new Set([...MAINTAIN_PROMPT.matchAll(/^-\s+([A-D]\d)\s/gm)].map((match) => match[1]));
52
62
  const KINDS = new Set([
53
63
  "skill-level",
54
64
  "relationship-level",
@@ -176,6 +186,7 @@ function validateAndNormalizeMaintainPlan(raw, report, validSignals) {
176
186
  }
177
187
  }
178
188
  if (!isNonEmptyString(item.rule)) errors.push(`${path}.rule: required`);
189
+ else if (!MAINTAIN_RULE_IDS.has(item.rule.trim())) errors.push(`${path}.rule: "${item.rule}" is not a clause id the maintain template defines (${[...MAINTAIN_RULE_IDS].sort().join(", ")})`);
179
190
  if (typeof item.finding !== "string" || item.finding.trim().length === 0) errors.push(`${path}.finding: required`);
180
191
  if (!isNonEmptyString(item.recommendation)) errors.push(`${path}.recommendation: required`);
181
192
  if (!isNonEmptyString(item.semantic_reasoning)) errors.push(`${path}.semantic_reasoning: required`);
@@ -238,7 +249,7 @@ function validateAndNormalizeMaintainPlan(raw, report, validSignals) {
238
249
  },
239
250
  forcedHuman
240
251
  };
241
- const unknownQualitySkills = new Set(report.skills.filter((skill) => findDriftSignal(skill.signals, "quality_low")?.verdict === "unknown").map((skill) => skill.name));
252
+ const unknownQualitySkills = new Set(report.skills.filter((skill) => findDriftSignal(skill.signals, "quality_low")?.verdict === "unknown").map((skill) => skill.name.trim()));
242
253
  for (const item of plan) if (item.names.some((name) => unknownQualitySkills.has(name)) && !item.needs_human) {
243
254
  item.needs_human = true;
244
255
  forcedHuman.push(...item.names.filter((name) => unknownQualitySkills.has(name)));
@@ -349,8 +360,10 @@ function formatPlan(validated, runId) {
349
360
  const { plan, forcedHuman } = validated;
350
361
  const lines = [];
351
362
  lines.push(`Maintenance scan ${runId}: verdict=${plan.verdict} (${plan.plan.length} recommendations, ${plan.notes.length} notes)`);
363
+ const noteLines = plan.notes.length === 0 ? [] : ["Notes:", ...plan.notes.map((note) => `- ${note}`)];
352
364
  if (plan.verdict === "no_issues") {
353
365
  lines.push("No drift issues detected. Nothing to do.");
366
+ lines.push(...noteLines);
354
367
  return lines.join("\n");
355
368
  }
356
369
  for (const item of plan.plan) {
@@ -367,10 +380,7 @@ function formatPlan(validated, runId) {
367
380
  if (item.is_override && item.override_reason) lines.push(` override: ${item.override_reason}`);
368
381
  }
369
382
  if (forcedHuman.length > 0) lines.push(`(quality_low gate: forced needs_human for ${[...new Set(forcedHuman)].join(", ")})`);
370
- if (plan.notes.length > 0) {
371
- lines.push("Notes:");
372
- for (const note of plan.notes) lines.push(`- ${note}`);
373
- }
383
+ lines.push(...noteLines);
374
384
  return lines.join("\n");
375
385
  }
376
386
  /** Run one maintenance scan and return display text plus validation metadata. */
@@ -383,6 +393,7 @@ async function runMaintain(runtime, options = {}) {
383
393
  error: "dsh-evolution prompt bundle integrity check failed; refusing to run maintain"
384
394
  };
385
395
  const usageObserved = options.usageObserved ? options.usageObserved() : void 0;
396
+ const readFailures = [];
386
397
  const snapshots = await snapshotFromLibrary(runtime.library, {
387
398
  supportFiles: options.supportFiles ? options.supportFiles() : void 0,
388
399
  descriptions: options.descriptions ? options.descriptions() : void 0,
@@ -391,16 +402,29 @@ async function runMaintain(runtime, options = {}) {
391
402
  catalogInvalid: options.catalogInvalid ? options.catalogInvalid() : void 0,
392
403
  usageObserved,
393
404
  onReadError: (name, error) => {
405
+ readFailures.push(name);
394
406
  runtime.logger?.warn(`evolution-maintenance: skipping unreadable skill "${name}": ${error instanceof Error ? error.message : String(error)}`);
395
407
  }
396
408
  });
397
- if (snapshots.length === 0) return {
398
- ok: true,
399
- recommendationCount: 0,
400
- runId: randomUUID(),
401
- verdict: "no_issues",
402
- text: "Maintenance scan: empty skill library. Nothing to do."
403
- };
409
+ if (snapshots.length === 0) {
410
+ if (readFailures.length > 0) return {
411
+ ok: false,
412
+ recommendationCount: 0,
413
+ error: `maintenance scan could not read ${readFailures.length} listed skill(s) (${readFailures.slice(0, 10).join(", ")}${readFailures.length > 10 ? ", …" : ""}) and read no skill at all — the library was NOT audited; fix the unreadable skills (or their directory names) and run the scan again`
414
+ };
415
+ if (runtime.rootExists && !await runtime.rootExists()) return {
416
+ ok: false,
417
+ recommendationCount: 0,
418
+ error: `maintenance scan found an empty skill library, but the configured skill root does not exist${runtime.skillRoot ? ` (${runtime.skillRoot})` : ""} — point the skillsRoot config at a real directory (a literal \`~\` is not expanded) and run the scan again`
419
+ };
420
+ return {
421
+ ok: true,
422
+ recommendationCount: 0,
423
+ runId: randomUUID(),
424
+ verdict: "no_issues",
425
+ text: "Maintenance scan: empty skill library. Nothing to do."
426
+ };
427
+ }
404
428
  const { facts, report, signalsVersion, signature } = buildMaintainFacts(snapshots, usageObserved, options.redact);
405
429
  const template = renderMaintainTemplate(MAINTAIN_PROMPT, PROMPT_BUNDLE_ID, signalsVersion, signature);
406
430
  const probeRegistered = runtime.tools?.get("maintenance_probe") != null;
@@ -410,14 +434,14 @@ async function runMaintain(runtime, options = {}) {
410
434
 
411
435
  ${MAINTAIN_OUTPUT_INSTRUCTION}`;
412
436
  const timeoutMs = options.timeoutMs ?? 6e5;
413
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) return {
437
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMER_DELAY_MS) return {
414
438
  ok: false,
415
439
  recommendationCount: 0,
416
- error: `maintain: --timeout must be a positive integer in ms, at most 2147483647; got ${String(timeoutMs)}`
440
+ error: `maintain: --timeout must be a positive integer in ms, at most ${MAX_TIMER_DELAY_MS}; got ${String(timeoutMs)}`
417
441
  };
418
442
  const signal = options.signal !== void 0 ? AbortSignal.any([AbortSignal.timeout(timeoutMs), options.signal]) : AbortSignal.timeout(timeoutMs);
419
443
  abortSignal = signal;
420
- const agentOptions = { model: options.model ?? runtime.evolutionPolicy?.get()?.curatorModel ?? "deepseek-v4-pro" };
444
+ const agentOptions = { model: options.model ?? runtime.evolutionPolicy?.get()?.curatorModel ?? DEFAULT_CURATOR_MODEL };
421
445
  if (options.provider) agentOptions.provider = options.provider;
422
446
  const run = await runtime.subagents.start("spawn", {
423
447
  label: "dsh-evolution-maintain",
@@ -550,4 +574,4 @@ ${MAINTAIN_OUTPUT_INSTRUCTION}`;
550
574
  }
551
575
  }
552
576
  //#endregion
553
- export { PROBE_SIGNALS, buildEnrichment, buildMaintainFacts, computeProbe, renderFacts, renderMaintainTemplate, runMaintain, snapshotFromLibrary, validateAndNormalizeMaintainPlan };
577
+ export { MAINTAIN_RULE_IDS, PROBE_SIGNALS, buildEnrichment, buildMaintainFacts, computeProbe, renderFacts, renderMaintainTemplate, runMaintain, snapshotFromLibrary, validateAndNormalizeMaintainPlan };
package/lib/tools.js CHANGED
@@ -1,5 +1,5 @@
1
- import { i as snapshotFromLibrary, n as PROBE_SIGNALS, r as computeProbe, t as buildEnrichment } from "./enrichment-CYpvRfH_.js";
2
- import { SkillLibrary, redactSecrets, resolveRootConfig, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
1
+ import { i as snapshotFromLibrary, n as PROBE_SIGNALS, r as computeProbe, t as buildEnrichment } from "./enrichment-2NEuOAZU.js";
2
+ import { SkillLibrary, assertSkillsRootAliasRetired, redactSecrets, resolveRootConfig, resolveSkillsRoot } from "@lmzhen/dsh-evolution-core";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
5
  //#region lib/types/tools.js
@@ -19,8 +19,8 @@ const Config = z.object({
19
19
  skillsRoot: z.string().default("")
20
20
  });
21
21
  function apply(ctx, rawConfig = {}) {
22
+ assertSkillsRootAliasRetired(rawConfig);
22
23
  const rootConfig = resolveRootConfig(rawConfig);
23
- if (rootConfig.usedDeprecatedAlias) ctx.logger.warn("evolution-maintenance-tools: config \"skillsRoot\" is deprecated (E-7); use \"root\" — the alias is honoured until 0.3.65");
24
24
  ctx.inject(["tools"], (toolCtx) => {
25
25
  const tools = toolCtx.get("tools");
26
26
  toolCtx.effect(() => tools.register(defineTool({
@@ -41,6 +41,14 @@ export interface MaintainRuntime {
41
41
  tools?: {
42
42
  get(name: string): unknown;
43
43
  } | undefined;
44
+ /** V27 M-02: does the configured skill root EXIST? The io seam maps a missing
45
+ * directory to an empty listing, so "the scan found no skills" could not be
46
+ * told apart from "the configured root is not there" (an unexpanded `~`, a
47
+ * typo — INSTALL.md routes users to absolute paths). Optional: without the
48
+ * probe the scan keeps the previous empty-library answer. */
49
+ rootExists?: (() => Promise<boolean>) | undefined;
50
+ /** V27 M-02: the root the probe above refers to, for the error message. */
51
+ skillRoot?: string | undefined;
44
52
  }
45
53
  export interface MaintainOptions {
46
54
  timeoutMs?: number;
@@ -15,8 +15,8 @@ export interface Config {
15
15
  /** Skill-tree root for probe reads; empty uses the default tree. E-7 (v18):
16
16
  * canonical key — the same `root` every other family row reads. */
17
17
  root?: string | undefined;
18
- /** Deprecated alias of `root` (E-7, v18); honoured only while `root` is
19
- * empty, with a warning; removed after 0.3.65. */
18
+ /** V27 G2.4 (M-08): RETIRED alias of `root`. Declared so the loader passes it
19
+ * to the load-time gate (which rejects it loudly) instead of dropping it. */
20
20
  skillsRoot?: string | undefined;
21
21
  }
22
22
  export declare const Config: z<Schemastery.ObjectS<{
@@ -8,6 +8,16 @@
8
8
  * recommendation is right — only whether it is well-formed and traceable.
9
9
  */
10
10
  import { type DriftReport } from '@lmzhen/dsh-evolution-core';
11
+ /**
12
+ * V27 D-3: the clause ids the maintain template ITSELF enumerates — the `- A1 …`
13
+ * bullets of its §5 rule catalogue. `rule` is checked against this set (parsed
14
+ * from the shipped template at module load, so a template edit that adds or
15
+ * renames a clause moves the set with it) instead of the former bare
16
+ * non-empty-string check, which let a plan cite a clause number that does not
17
+ * exist and still pass mechanical validation. `self-consistency.spec.ts` pins
18
+ * the parsed set so a template rewording cannot silently empty it.
19
+ */
20
+ export declare const MAINTAIN_RULE_IDS: ReadonlySet<string>;
11
21
  export type MaintainVerdict = 'issues' | 'no_issues';
12
22
  export type MaintainPlanItemKind = 'skill-level' | 'relationship-level' | 'library-level';
13
23
  export type MaintainReversibility = 'archive' | 'restructure' | 'patch' | 'rename' | 'none';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-maintenance",
3
3
  "description": "Deterministic maintenance-scan surface: skill-library snapshot assembly, drift signals and mechanical-facts rendering (design 011) (community build)",
4
- "version": "0.3.67",
4
+ "version": "0.3.69",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -35,7 +35,7 @@
35
35
  "license": "MIT",
36
36
  "dependencies": {
37
37
  "@deepseek-ai/schemastery": "^3.18.1",
38
- "@lmzhen/dsh-evolution-core": "^0.3.67"
38
+ "@lmzhen/dsh-evolution-core": "^0.3.69"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
@@ -43,6 +43,7 @@
43
43
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
44
44
  },
45
45
  "devDependencies": {
46
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
46
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
47
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
47
48
  }
48
49
  }