@lmzhen/dsh-evolution-maintenance 0.3.66 → 0.3.68

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`);
@@ -224,6 +235,10 @@ function validateAndNormalizeMaintainPlan(raw, report, validSignals) {
224
235
  const uncovered = [...overSignals].filter((id) => !covered.has(id) && !notesText.includes(id));
225
236
  if (uncovered.length > 0) errors.push(`completeness: over signal(s) not covered by any plan evidence nor named in a note (${uncovered.join(", ")}) — §3 requires each over signal to be addressed or explicitly explained away`);
226
237
  }
238
+ else if (verdict === "no_issues" && overSignals.size > 0) {
239
+ const uncovered = [...overSignals].filter((id) => !notesText.includes(id));
240
+ if (uncovered.length > 0) errors.push(`completeness: verdict=no_issues while the facts block carries over signal(s) not named in any note (${uncovered.join(", ")}) — each signal must be explicitly explained away in notes (§3/§7)`);
241
+ }
227
242
  if (errors.length > 0) return {
228
243
  ok: false,
229
244
  errors,
@@ -234,7 +249,7 @@ function validateAndNormalizeMaintainPlan(raw, report, validSignals) {
234
249
  },
235
250
  forcedHuman
236
251
  };
237
- 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()));
238
253
  for (const item of plan) if (item.names.some((name) => unknownQualitySkills.has(name)) && !item.needs_human) {
239
254
  item.needs_human = true;
240
255
  forcedHuman.push(...item.names.filter((name) => unknownQualitySkills.has(name)));
@@ -345,8 +360,10 @@ function formatPlan(validated, runId) {
345
360
  const { plan, forcedHuman } = validated;
346
361
  const lines = [];
347
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}`)];
348
364
  if (plan.verdict === "no_issues") {
349
365
  lines.push("No drift issues detected. Nothing to do.");
366
+ lines.push(...noteLines);
350
367
  return lines.join("\n");
351
368
  }
352
369
  for (const item of plan.plan) {
@@ -363,10 +380,7 @@ function formatPlan(validated, runId) {
363
380
  if (item.is_override && item.override_reason) lines.push(` override: ${item.override_reason}`);
364
381
  }
365
382
  if (forcedHuman.length > 0) lines.push(`(quality_low gate: forced needs_human for ${[...new Set(forcedHuman)].join(", ")})`);
366
- if (plan.notes.length > 0) {
367
- lines.push("Notes:");
368
- for (const note of plan.notes) lines.push(`- ${note}`);
369
- }
383
+ lines.push(...noteLines);
370
384
  return lines.join("\n");
371
385
  }
372
386
  /** Run one maintenance scan and return display text plus validation metadata. */
@@ -379,6 +393,7 @@ async function runMaintain(runtime, options = {}) {
379
393
  error: "dsh-evolution prompt bundle integrity check failed; refusing to run maintain"
380
394
  };
381
395
  const usageObserved = options.usageObserved ? options.usageObserved() : void 0;
396
+ const readFailures = [];
382
397
  const snapshots = await snapshotFromLibrary(runtime.library, {
383
398
  supportFiles: options.supportFiles ? options.supportFiles() : void 0,
384
399
  descriptions: options.descriptions ? options.descriptions() : void 0,
@@ -387,16 +402,29 @@ async function runMaintain(runtime, options = {}) {
387
402
  catalogInvalid: options.catalogInvalid ? options.catalogInvalid() : void 0,
388
403
  usageObserved,
389
404
  onReadError: (name, error) => {
405
+ readFailures.push(name);
390
406
  runtime.logger?.warn(`evolution-maintenance: skipping unreadable skill "${name}": ${error instanceof Error ? error.message : String(error)}`);
391
407
  }
392
408
  });
393
- if (snapshots.length === 0) return {
394
- ok: true,
395
- recommendationCount: 0,
396
- runId: randomUUID(),
397
- verdict: "no_issues",
398
- text: "Maintenance scan: empty skill library. Nothing to do."
399
- };
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
+ }
400
428
  const { facts, report, signalsVersion, signature } = buildMaintainFacts(snapshots, usageObserved, options.redact);
401
429
  const template = renderMaintainTemplate(MAINTAIN_PROMPT, PROMPT_BUNDLE_ID, signalsVersion, signature);
402
430
  const probeRegistered = runtime.tools?.get("maintenance_probe") != null;
@@ -406,14 +434,14 @@ async function runMaintain(runtime, options = {}) {
406
434
 
407
435
  ${MAINTAIN_OUTPUT_INSTRUCTION}`;
408
436
  const timeoutMs = options.timeoutMs ?? 6e5;
409
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) return {
437
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_TIMER_DELAY_MS) return {
410
438
  ok: false,
411
439
  recommendationCount: 0,
412
- 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)}`
413
441
  };
414
442
  const signal = options.signal !== void 0 ? AbortSignal.any([AbortSignal.timeout(timeoutMs), options.signal]) : AbortSignal.timeout(timeoutMs);
415
443
  abortSignal = signal;
416
- 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 };
417
445
  if (options.provider) agentOptions.provider = options.provider;
418
446
  const run = await runtime.subagents.start("spawn", {
419
447
  label: "dsh-evolution-maintain",
@@ -546,4 +574,4 @@ ${MAINTAIN_OUTPUT_INSTRUCTION}`;
546
574
  }
547
575
  }
548
576
  //#endregion
549
- 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.66",
4
+ "version": "0.3.68",
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.66"
38
+ "@lmzhen/dsh-evolution-core": "^0.3.68"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",