@lmzhen/dsh-evolution-maintenance 0.0.0 → 0.3.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/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @deepseek-ai/dsh-evolution-maintenance
2
+
3
+ Skill-library drift-scanning determinism surface for the maintenance subagent
4
+ (design `docs/design-review/011-maintenance-subagent-v2.md`).
5
+
6
+ - `snapshotFromLibrary` — assemble a plain skill snapshot from a SkillLibrary-like reader.
7
+ - `renderFacts` — canonical `MECHANICAL_FACTS` block (version + joint signature + redaction).
8
+
9
+ Signal computation lives in `@deepseek-ai/dsh-evolution-core` (`drift-signals`); this
10
+ package owns assembly and rendering only.
11
+
12
+ ## Known Limitations and Deferred Work
13
+
14
+ - Phase 1-2 expose no service beyond the command surface: the chain
15
+ (commands → scan → render → subagent → validate) is wired through
16
+ `/evolution maintain`; orchestration lives in this package.
17
+ - `maintenance_probe` (read-only deep-dive tool, host-mounted via
18
+ `evolution-maintenance-tools`) is available to maintenance subagents only
19
+ through the orchestrate `toolFilter` allow-list; it is globally visible to
20
+ every session as a read-only query (same exposure tier as the `skill`
21
+ tool — never a write path).
22
+ - The model-visible template (`MAINTAIN_PROMPT`) ships in `evolution-core`
23
+ `PROMPT_BUNDLE`; the joint-signature mismatch protocol is honored by
24
+ `renderFacts` callers, not by this package alone.
package/lib/index.js ADDED
@@ -0,0 +1,448 @@
1
+ import { n as computeProbe, t as PROBE_SIGNALS } from "./probe-CrVeRJ6b.js";
2
+ import { AUTHORING_DESCRIPTION_BAR, DEFAULT_HEALTH_THRESHOLDS, DRIFT_MAX_LINE_CHARS, DRIFT_SIGNALS_VERSION, DRIFT_SIGNAL_NOUNS, LOW_QUALITY_THRESHOLD, MAINTAIN_PROMPT, PROMPT_BUNDLE, PROMPT_BUNDLE_ID, computeDriftSignals, findDriftSignal, redactSecrets, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ //#region lib/types/drift-scan.js
5
+ /**
6
+ * Skill-library snapshot assembly for drift scanning.
7
+ *
8
+ * `computeDriftSignals` (evolution-core) is a pure function over a plain
9
+ * snapshot; this module converts a SkillLibrary-like reader into that
10
+ * snapshot. Quality score and usage-window status are caller-provided —
11
+ * when absent they stay `undefined`, which the signal layer reports as
12
+ * `unknown` (never a fabricated verdict).
13
+ */
14
+ /**
15
+ * Assemble drift snapshots from a library reader and optional enrichment.
16
+ * Missing enrichment stays `undefined` → signal layer reports `unknown`.
17
+ */
18
+ async function snapshotFromLibrary(library, options = {}) {
19
+ const entries = await library.list();
20
+ const snapshots = [];
21
+ for (const entry of entries) {
22
+ const body = await library.read(entry.name);
23
+ if (body === void 0 || body === null) continue;
24
+ snapshots.push({
25
+ name: entry.name,
26
+ body,
27
+ description: options.descriptions?.get(entry.name),
28
+ supportFiles: options.supportFiles?.get(entry.name),
29
+ quality: options.quality?.get(entry.name)
30
+ });
31
+ }
32
+ return snapshots;
33
+ }
34
+ //#endregion
35
+ //#region lib/types/render-facts.js
36
+ /**
37
+ * Mechanical-facts rendering for the maintenance subagent (011 §6).
38
+ *
39
+ * One canonical block: opening tag with version + joint signature, one
40
+ * `[FACT]`/`[UNKNOWN]` line per signal, closing tag. The template side
41
+ * renders its own `<MAINTAIN_PROMPT>` header with the same signature, so
42
+ * the model can compare the two heads (011 mismatch protocol). All rendered
43
+ * value/detail text passes through the optional redactor before it leaves
44
+ * the session.
45
+ */
46
+ function renderFacts(report, options) {
47
+ const redact = options.redact ?? redactSecrets;
48
+ const lines = [];
49
+ lines.push(`<<<MECHANICAL_FACTS v=${options.signalsVersion} sig=${options.signature}>>>`);
50
+ for (const signal of report.library) lines.push(...renderSignal(signal, redact));
51
+ for (const skill of report.skills) {
52
+ lines.push(`# skill=${skill.name}`);
53
+ for (const signal of skill.signals) lines.push(...renderSignal(signal, redact));
54
+ }
55
+ lines.push("<<<END FACTS>>>");
56
+ return lines.join("\n");
57
+ }
58
+ function renderSignal(signal, redact) {
59
+ const prefix = signal.verdict === "unknown" ? "[UNKNOWN]" : "[FACT]";
60
+ const value = redact(signal.value);
61
+ const parts = [
62
+ `${prefix} signal=${signal.id}`,
63
+ `value=${value}`,
64
+ `verdict=${signal.verdict}`
65
+ ];
66
+ if (signal.threshold !== void 0) parts.push(`threshold=${signal.threshold}`);
67
+ if (signal.detail !== void 0) parts.push(`detail=${redact(signal.detail)}`);
68
+ return [parts.join(" ")];
69
+ }
70
+ /** Convenience for tests and replays: summarize a skill assessment. */
71
+ function summarizeAssessment(assessment) {
72
+ const over = assessment.signals.filter((signal) => signal.verdict === "over").map((signal) => signal.id);
73
+ return over.length === 0 ? `${assessment.name}: clean` : `${assessment.name}: ${over.join(",")}`;
74
+ }
75
+ //#endregion
76
+ //#region lib/types/validate-plan.js
77
+ /**
78
+ * Maintain-plan validation and normalization (011 §7).
79
+ *
80
+ * Contract checks (mechanical, no semantics): enum membership, required
81
+ * fields, evidence closed over the facts block's signal set, needs_human
82
+ * consistency with the confidence/reversibility/is_override/unknown rules,
83
+ * and the quality_low=unknown global imposition. Never judges whether a
84
+ * recommendation is right — only whether it is well-formed and traceable.
85
+ */
86
+ const KINDS = new Set([
87
+ "skill-level",
88
+ "relationship-level",
89
+ "library-level"
90
+ ]);
91
+ const REVERSIBILITIES = new Set([
92
+ "archive",
93
+ "restructure",
94
+ "patch",
95
+ "rename",
96
+ "none"
97
+ ]);
98
+ const IMPACTS = new Set([
99
+ "better",
100
+ "worse",
101
+ "neutral"
102
+ ]);
103
+ function isRecord(value) {
104
+ return typeof value === "object" && value !== null && !Array.isArray(value);
105
+ }
106
+ function str(value, fallback) {
107
+ return isNonEmptyString(value) ? value : fallback;
108
+ }
109
+ function isNonEmptyString(value) {
110
+ return typeof value === "string" && value.trim().length > 0;
111
+ }
112
+ function validateEvidence(value, validSignals, errors, path) {
113
+ if (!Array.isArray(value) || value.length === 0) {
114
+ errors.push(`${path}.evidence: must be a non-empty array`);
115
+ return [];
116
+ }
117
+ const out = [];
118
+ const arr = value;
119
+ for (const index of arr.keys()) {
120
+ const entry = arr[index];
121
+ if (!isRecord(entry) || !isNonEmptyString(entry.signal) || !isNonEmptyString(entry.value)) {
122
+ errors.push(`${path}.evidence[${index}]: must be {signal, value} strings`);
123
+ continue;
124
+ }
125
+ if (!validSignals.has(entry.signal)) errors.push(`${path}.evidence[${index}].signal="${entry.signal}": not in the facts block`);
126
+ out.push({
127
+ signal: entry.signal,
128
+ value: entry.value
129
+ });
130
+ }
131
+ return out;
132
+ }
133
+ /**
134
+ * Validate and normalize a maintain plan against the facts report.
135
+ * @param raw - subagent output (already JSON-parsed by the subagent channel).
136
+ * @param report - the DriftReport rendered into the facts block (quality_low gate source).
137
+ * @param validSignals - ids present in the rendered facts block.
138
+ */
139
+ function validateAndNormalizeMaintainPlan(raw, report, validSignals) {
140
+ const errors = [];
141
+ const forcedHuman = [];
142
+ if (!isRecord(raw)) return {
143
+ ok: false,
144
+ errors: ["plan root: must be an object"],
145
+ plan: {
146
+ verdict: "no_issues",
147
+ plan: [],
148
+ notes: []
149
+ },
150
+ forcedHuman
151
+ };
152
+ const verdict = raw.verdict;
153
+ if (verdict !== "issues" && verdict !== "no_issues") errors.push(`verdict: must be "issues" or "no_issues", got ${String(verdict)}`);
154
+ const plan = [];
155
+ if (!Array.isArray(raw.plan)) errors.push("plan: must be an array");
156
+ else {
157
+ const rawPlan = raw.plan;
158
+ if (verdict === "no_issues" && rawPlan.length > 0) errors.push("verdict=no_issues with a non-empty plan");
159
+ for (const index of rawPlan.keys()) {
160
+ const item = rawPlan[index];
161
+ const path = `plan[${index}]`;
162
+ if (!isRecord(item)) {
163
+ errors.push(`${path}: must be an object`);
164
+ continue;
165
+ }
166
+ if (typeof item.kind !== "string" || !KINDS.has(item.kind)) errors.push(`${path}.kind: invalid`);
167
+ if (!Array.isArray(item.names) || item.names.length === 0 || !item.names.every(isNonEmptyString)) errors.push(`${path}.names: non-empty string array required`);
168
+ if (!isNonEmptyString(item.rule)) errors.push(`${path}.rule: required`);
169
+ if (typeof item.finding !== "string" || item.finding.trim().length === 0) errors.push(`${path}.finding: required`);
170
+ if (!isNonEmptyString(item.recommendation)) errors.push(`${path}.recommendation: required`);
171
+ if (!isNonEmptyString(item.semantic_reasoning)) errors.push(`${path}.semantic_reasoning: required`);
172
+ if (typeof item.impact !== "string" || !IMPACTS.has(item.impact)) errors.push(`${path}.impact: invalid`);
173
+ if (!isNonEmptyString(item.impact_reason)) errors.push(`${path}.impact_reason: required`);
174
+ if (typeof item.reversibility !== "string" || !REVERSIBILITIES.has(item.reversibility)) errors.push(`${path}.reversibility: invalid`);
175
+ if (!isNonEmptyString(item.undo_path)) errors.push(`${path}.undo_path: required`);
176
+ if (typeof item.confidence !== "number" || !Number.isFinite(item.confidence) || item.confidence < 0 || item.confidence > 1) errors.push(`${path}.confidence: finite number in [0,1] required`);
177
+ if (typeof item.needs_human !== "boolean") errors.push(`${path}.needs_human: boolean required`);
178
+ if (typeof item.is_override !== "boolean") errors.push(`${path}.is_override: boolean required`);
179
+ if (item.is_override === true && !isNonEmptyString(item.override_reason)) errors.push(`${path}.override_reason: required when is_override`);
180
+ const evidence = validateEvidence(item.evidence, validSignals, errors, path);
181
+ plan.push({
182
+ kind: isNonEmptyString(item.kind) ? item.kind : "skill-level",
183
+ names: Array.isArray(item.names) ? item.names : [],
184
+ rule: str(item.rule, ""),
185
+ evidence,
186
+ finding: str(item.finding, ""),
187
+ recommendation: str(item.recommendation, ""),
188
+ semantic_reasoning: str(item.semantic_reasoning, ""),
189
+ impact: isNonEmptyString(item.impact) ? item.impact : "neutral",
190
+ impact_reason: str(item.impact_reason, ""),
191
+ reversibility: isNonEmptyString(item.reversibility) ? item.reversibility : "none",
192
+ undo_path: str(item.undo_path, ""),
193
+ confidence: typeof item.confidence === "number" ? item.confidence : 0,
194
+ needs_human: item.needs_human === true,
195
+ is_override: item.is_override === true,
196
+ override_reason: isNonEmptyString(item.override_reason) ? item.override_reason : void 0
197
+ });
198
+ }
199
+ }
200
+ const notes = Array.isArray(raw.notes) ? raw.notes.filter((note) => isNonEmptyString(note)) : [];
201
+ if (errors.length > 0) return {
202
+ ok: false,
203
+ errors,
204
+ plan: {
205
+ verdict: verdict === "no_issues" ? "no_issues" : "issues",
206
+ plan,
207
+ notes
208
+ },
209
+ forcedHuman
210
+ };
211
+ const unknownQualitySkills = new Set(report.skills.filter((skill) => findDriftSignal(skill.signals, "quality_low")?.verdict === "unknown").map((skill) => skill.name));
212
+ for (const item of plan) if (item.names.some((name) => unknownQualitySkills.has(name)) && !item.needs_human) {
213
+ item.needs_human = true;
214
+ forcedHuman.push(...item.names.filter((name) => unknownQualitySkills.has(name)));
215
+ }
216
+ for (const item of plan) {
217
+ const referencesUnknown = item.evidence.some((ev) => {
218
+ return findSignalInReport(report, ev.signal)?.verdict === "unknown";
219
+ });
220
+ const lowConfidence = item.confidence < .6;
221
+ const irreversible = item.reversibility === "rename" || item.reversibility === "none";
222
+ if (!item.needs_human && (lowConfidence || irreversible || item.is_override || referencesUnknown)) item.needs_human = true;
223
+ }
224
+ return {
225
+ ok: true,
226
+ errors: [],
227
+ plan: {
228
+ verdict,
229
+ plan,
230
+ notes
231
+ },
232
+ forcedHuman
233
+ };
234
+ }
235
+ function findSignalInReport(report, id) {
236
+ return report.library.find((signal) => signal.id === id) ?? report.skills.flatMap((skill) => skill.signals).find((signal) => signal.id === id);
237
+ }
238
+ //#endregion
239
+ //#region lib/types/orchestrate.js
240
+ /**
241
+ * Maintain orchestration (011 §3/§7): snapshot → drift signals → facts
242
+ * render → subagent (template M + facts) → validate/normalize → display
243
+ * text. Deterministic parts stay pure; the subagent call is the only
244
+ * external dependency (injected, fake-able in tests).
245
+ */
246
+ function existingSignalIds(report) {
247
+ const ids = /* @__PURE__ */ new Set();
248
+ for (const signal of report.library) ids.add(signal.id);
249
+ for (const skill of report.skills) for (const signal of skill.signals) ids.add(signal.id);
250
+ return ids;
251
+ }
252
+ /** Render template-M placeholders from the signal vocabulary (011 single-source rule). */
253
+ function renderMaintainTemplate(template, bundleVersion, signalsVersion, signature) {
254
+ let out = template;
255
+ out = out.replace(/{\s*signal:([a-z_]+)\.threshold\s*}/g, (_, id) => thresholdNoun(id));
256
+ out = out.replace(/{\s*signal:([a-z_]+)\s*}/g, (_, id) => DRIFT_SIGNAL_NOUNS[id] ?? id);
257
+ out = out.replaceAll("{bundle_version}", bundleVersion);
258
+ out = out.replaceAll("{signals_version}", signalsVersion);
259
+ out = out.replaceAll("{joint_signature}", signature);
260
+ return out;
261
+ }
262
+ function thresholdNoun(id) {
263
+ if (id === "stamp_density") return `${DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb}/KB`;
264
+ if (id === "description_chars") return `${AUTHORING_DESCRIPTION_BAR}`;
265
+ if (id === "body_size") return `${DEFAULT_HEALTH_THRESHOLDS.softBodyChars}`;
266
+ if (id === "overlong_line") return `${DRIFT_MAX_LINE_CHARS}`;
267
+ return `(阈 ${id})`;
268
+ }
269
+ function jointSignature(template, signalsVersion) {
270
+ const canonical = JSON.stringify({
271
+ signalsVersion,
272
+ template,
273
+ nouns: DRIFT_SIGNAL_NOUNS,
274
+ thresholds: {
275
+ stampDensityPerKb: DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb,
276
+ minStampBodyChars: 2e3,
277
+ softBodyChars: DEFAULT_HEALTH_THRESHOLDS.softBodyChars,
278
+ descriptionChars: AUTHORING_DESCRIPTION_BAR,
279
+ qualityLow: LOW_QUALITY_THRESHOLD,
280
+ maxLineChars: DRIFT_MAX_LINE_CHARS
281
+ }
282
+ });
283
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
284
+ }
285
+ /**
286
+ * Deterministic half of a maintenance scan: snapshots → drift report →
287
+ * rendered facts block (joint signature + redaction). Shared by `runMaintain`
288
+ * and the `--facts` preview so the preview can never disagree with the scan.
289
+ */
290
+ function buildMaintainFacts(snapshots, usageObserved, redact) {
291
+ const report = computeDriftSignals(snapshots.map((snapshot) => ({
292
+ ...snapshot,
293
+ usageObserved
294
+ })));
295
+ const signalsVersion = DRIFT_SIGNALS_VERSION;
296
+ const signature = jointSignature(MAINTAIN_PROMPT, signalsVersion);
297
+ return {
298
+ report,
299
+ facts: renderFacts(report, {
300
+ signalsVersion,
301
+ signature,
302
+ redact
303
+ }),
304
+ signalsVersion,
305
+ signature
306
+ };
307
+ }
308
+ function formatPlan(validated, runId) {
309
+ const { plan, forcedHuman } = validated;
310
+ const lines = [];
311
+ lines.push(`Maintenance scan ${runId}: verdict=${plan.verdict} (${plan.plan.length} recommendations, ${plan.notes.length} notes)`);
312
+ if (plan.verdict === "no_issues") {
313
+ lines.push("No drift issues detected. Nothing to do.");
314
+ return lines.join("\n");
315
+ }
316
+ for (const item of plan.plan) {
317
+ const flags = [
318
+ item.impact,
319
+ `rev=${item.reversibility}`,
320
+ `conf=${item.confidence.toFixed(2)}`,
321
+ item.needs_human ? "HUMAN" : ""
322
+ ].filter(Boolean);
323
+ lines.push(`- [${item.kind}] ${item.names.join(", ")} · rule=${item.rule} · ${flags.join(" ")}`);
324
+ lines.push(` finding: ${item.finding}`);
325
+ lines.push(` action: ${item.recommendation}`);
326
+ if (item.undo_path && item.undo_path !== "n/a") lines.push(` undo: ${item.undo_path}`);
327
+ if (item.is_override && item.override_reason) lines.push(` override: ${item.override_reason}`);
328
+ }
329
+ if (forcedHuman.length > 0) lines.push(`(quality_low gate: forced needs_human for ${[...new Set(forcedHuman)].join(", ")})`);
330
+ if (plan.notes.length > 0) {
331
+ lines.push("Notes:");
332
+ for (const note of plan.notes) lines.push(`- ${note}`);
333
+ }
334
+ return lines.join("\n");
335
+ }
336
+ /** Run one maintenance scan and return display text plus validation metadata. */
337
+ async function runMaintain(runtime, options = {}) {
338
+ try {
339
+ if (!verifyPromptBundle(PROMPT_BUNDLE)) return {
340
+ ok: false,
341
+ error: "dsh-evolution prompt bundle integrity check failed; refusing to run maintain"
342
+ };
343
+ const snapshots = await snapshotFromLibrary(runtime.library, {
344
+ supportFiles: options.supportFiles ? options.supportFiles() : void 0,
345
+ descriptions: options.descriptions ? options.descriptions() : void 0,
346
+ quality: options.quality ? options.quality() : void 0
347
+ });
348
+ if (snapshots.length === 0) return {
349
+ ok: true,
350
+ runId: randomUUID(),
351
+ verdict: "no_issues",
352
+ text: "Maintenance scan: empty skill library. Nothing to do."
353
+ };
354
+ const { facts, report, signalsVersion, signature } = buildMaintainFacts(snapshots, options.usageObserved ? options.usageObserved() : void 0, options.redact);
355
+ const template = renderMaintainTemplate(MAINTAIN_PROMPT, PROMPT_BUNDLE_ID, signalsVersion, signature);
356
+ const prompt = `${facts}
357
+
358
+ 按模板契约输出 JSON 维护计划(verdict/plan/notes);除 skill 工具与维护模板外你无其他工具。`;
359
+ const timeoutMs = options.timeoutMs ?? 12e4;
360
+ const agentOptions = { model: options.model ?? "deepseek-v4-pro" };
361
+ if (options.provider) agentOptions.provider = options.provider;
362
+ const raw = (await (await runtime.subagents.start("spawn", {
363
+ label: "dsh-evolution-maintain",
364
+ prompt: [{
365
+ type: "text",
366
+ text: prompt
367
+ }],
368
+ parent: runtime.parent,
369
+ signal: AbortSignal.timeout(timeoutMs),
370
+ maxDepth: options.maxDepth ?? 0,
371
+ agentOptions,
372
+ persona: template,
373
+ toolFilter: { allow: [...options.toolAllow ?? ["skill", "maintenance_probe"]] },
374
+ outputSchema: {
375
+ type: "object",
376
+ additionalProperties: false,
377
+ properties: {
378
+ verdict: { type: "string" },
379
+ plan: {
380
+ type: "array",
381
+ items: {
382
+ type: "object",
383
+ additionalProperties: false,
384
+ properties: {
385
+ kind: { type: "string" },
386
+ names: {
387
+ type: "array",
388
+ items: { type: "string" }
389
+ },
390
+ rule: { type: "string" },
391
+ evidence: {
392
+ type: "array",
393
+ items: {
394
+ type: "object",
395
+ additionalProperties: false,
396
+ properties: {
397
+ signal: { type: "string" },
398
+ value: { type: "string" }
399
+ }
400
+ }
401
+ },
402
+ finding: { type: "string" },
403
+ recommendation: { type: "string" },
404
+ semantic_reasoning: { type: "string" },
405
+ impact: { type: "string" },
406
+ impact_reason: { type: "string" },
407
+ reversibility: { type: "string" },
408
+ undo_path: { type: "string" },
409
+ confidence: { type: "number" },
410
+ needs_human: { type: "boolean" },
411
+ is_override: { type: "boolean" },
412
+ override_reason: { type: "string" }
413
+ }
414
+ }
415
+ },
416
+ notes: {
417
+ type: "array",
418
+ items: { type: "string" }
419
+ }
420
+ }
421
+ }
422
+ })).result)?.structured;
423
+ if (raw === void 0) return {
424
+ ok: false,
425
+ error: "Maintain subagent returned no structured plan."
426
+ };
427
+ const validated = validateAndNormalizeMaintainPlan(raw, report, existingSignalIds(report));
428
+ if (!validated.ok) return {
429
+ ok: false,
430
+ error: `Maintain plan rejected by validator: ${validated.errors.slice(0, 5).join("; ")}`
431
+ };
432
+ const runId = randomUUID();
433
+ return {
434
+ ok: true,
435
+ runId,
436
+ verdict: validated.plan.verdict,
437
+ forcedHuman: validated.forcedHuman,
438
+ text: formatPlan(validated, runId)
439
+ };
440
+ } catch (error) {
441
+ return {
442
+ ok: false,
443
+ error: String(error)
444
+ };
445
+ }
446
+ }
447
+ //#endregion
448
+ export { PROBE_SIGNALS, buildMaintainFacts, computeProbe, renderFacts, renderMaintainTemplate, runMaintain, snapshotFromLibrary, summarizeAssessment, validateAndNormalizeMaintainPlan };
@@ -0,0 +1,8 @@
1
+ //#region lib/types/invariant.js
2
+ const PACKAGE_NAME = "@deepseek-ai/dsh-evolution-maintenance";
3
+ const name = "evolution-maintenance-invariant";
4
+ const inject = ["invariants"];
5
+ const install = () => {};
6
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
7
+ //#endregion
8
+ export { apply, inject, name };
package/lib/tools.js ADDED
@@ -0,0 +1,83 @@
1
+ import { n as computeProbe, t as PROBE_SIGNALS } from "./probe-CrVeRJ6b.js";
2
+ import { SkillLibrary, redactSecrets } from "@deepseek-ai/dsh-evolution-core";
3
+ import { defineTool } from "@deepseek-ai/dsh-tools";
4
+ //#region lib/types/tools.js
5
+ /**
6
+ * `maintenance_probe` tool (011 Phase 3).
7
+ *
8
+ * Read-only deep-dive: returns single-source machine detail for one signal
9
+ * (library-level group/cluster membership, or per-skill line/pointer/shape
10
+ * evidence). The subagent may use it to sharpen confidence and
11
+ * semantic_reasoning; it must never introduce evidence ids outside the facts
12
+ * block (validated plan-side). No write path exists in this tool.
13
+ * @module @deepseek-ai/dsh-evolution-maintenance-tools
14
+ */
15
+ const name = "evolution-maintenance-tools";
16
+ function apply(ctx, rawConfig = {}) {
17
+ const config = rawConfig;
18
+ ctx.inject(["tools"], (toolCtx) => {
19
+ toolCtx.tools.register(defineTool({
20
+ name: "maintenance_probe",
21
+ description: "Read-only deep-dive into maintenance scan signals: library-level group/cluster membership or per-skill detail (line numbers, pointer gaps, narrow shapes, stamp samples). Machine-derived from the same calculators as the facts block — never introduces new evidence ids. Output is JSON detail.",
22
+ parameters: {
23
+ signal: {
24
+ type: "string",
25
+ required: true,
26
+ enum: PROBE_SIGNALS
27
+ },
28
+ target: {
29
+ type: "string",
30
+ description: "Skill name for skill-level signals (required for stamp_density/body_size/dup_heading/overlong_line/pointer_missing/narrow_name/description_chars/quality_low)."
31
+ }
32
+ },
33
+ output: {
34
+ schema: {
35
+ type: "object",
36
+ additionalProperties: false,
37
+ properties: {
38
+ signal: { type: "string" },
39
+ target: { type: "string" },
40
+ detail: {
41
+ type: "array",
42
+ items: { type: "string" }
43
+ }
44
+ }
45
+ },
46
+ render: (_args, value) => [{
47
+ type: "text",
48
+ text: (value.detail ?? []).join("\n")
49
+ }]
50
+ },
51
+ isConcurrencySafe: () => true,
52
+ async execute(args) {
53
+ const signal = args.signal ?? "";
54
+ const target = args.target;
55
+ const ioRegistry = ctx.get("evolutionIo");
56
+ if (!ioRegistry) return {
57
+ signal,
58
+ detail: ["evolution-io registry not mounted"],
59
+ ...target ? { target } : {}
60
+ };
61
+ const library = new SkillLibrary(config.skillsRoot, ioRegistry.provider());
62
+ const entries = await library.list();
63
+ const snapshots = [];
64
+ for (const entry of entries) {
65
+ const body = await library.read(entry.name);
66
+ if (body === null) continue;
67
+ snapshots.push({
68
+ name: entry.name,
69
+ body
70
+ });
71
+ }
72
+ const probe = computeProbe(signal, target, snapshots);
73
+ const redacted = redactSecrets(probe.detail.join("\n"));
74
+ return {
75
+ ...probe,
76
+ detail: redacted.split("\n")
77
+ };
78
+ }
79
+ }));
80
+ });
81
+ }
82
+ //#endregion
83
+ export { apply, name };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Skill-library snapshot assembly for drift scanning.
3
+ *
4
+ * `computeDriftSignals` (evolution-core) is a pure function over a plain
5
+ * snapshot; this module converts a SkillLibrary-like reader into that
6
+ * snapshot. Quality score and usage-window status are caller-provided —
7
+ * when absent they stay `undefined`, which the signal layer reports as
8
+ * `unknown` (never a fabricated verdict).
9
+ */
10
+ import type { DriftSkillSnapshot } from '@deepseek-ai/dsh-evolution-core';
11
+ /** Minimal SkillLibrary surface needed for snapshot assembly. */
12
+ export interface SkillLibraryLike {
13
+ list(): Promise<ReadonlyArray<{
14
+ name: string;
15
+ }>>;
16
+ read(name: string): Promise<string | null | undefined>;
17
+ }
18
+ export interface SnapshotOptions {
19
+ /** Support-file relative paths per skill name, when the caller has them. */
20
+ supportFiles?: ReadonlyMap<string, readonly string[]> | undefined;
21
+ /** Parsed frontmatter description per skill name, when available. */
22
+ descriptions?: ReadonlyMap<string, string> | undefined;
23
+ /** Quality score per skill name, when the caller computed it. */
24
+ quality?: ReadonlyMap<string, number> | undefined;
25
+ }
26
+ /**
27
+ * Assemble drift snapshots from a library reader and optional enrichment.
28
+ * Missing enrichment stays `undefined` → signal layer reports `unknown`.
29
+ */
30
+ export declare function snapshotFromLibrary(library: SkillLibraryLike, options?: SnapshotOptions): Promise<DriftSkillSnapshot[]>;
31
+ //# sourceMappingURL=drift-scan.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Maintenance-scan determinism surface (design 011, Phase 1).
3
+ *
4
+ * Snapshot assembly (library → drift snapshot) and mechanical-facts
5
+ * rendering. Pure functions: no IO inside signal computation; the assembled
6
+ * snapshot is handed to `computeDriftSignals` (evolution-core) untouched.
7
+ * The full service chain (commands → scan → render → subagent → validate)
8
+ * is Phase 2; this module is the deterministic half, unit-testable without
9
+ * a live library.
10
+ * @module @deepseek-ai/dsh-evolution-maintenance
11
+ */
12
+ export * from './drift-scan.ts';
13
+ export * from './render-facts.ts';
14
+ export * from './validate-plan.ts';
15
+ export * from './orchestrate.ts';
16
+ export * from './probe.ts';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "evolution-maintenance-invariant";
3
+ export declare const inject: string[];
4
+ export declare const apply: (ctx: Context) => Promise<() => void>;
5
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Maintain orchestration (011 §3/§7): snapshot → drift signals → facts
3
+ * render → subagent (template M + facts) → validate/normalize → display
4
+ * text. Deterministic parts stay pure; the subagent call is the only
5
+ * external dependency (injected, fake-able in tests).
6
+ */
7
+ import { type DriftReport, type DriftSkillSnapshot } from '@deepseek-ai/dsh-evolution-core';
8
+ import { type SkillLibraryLike } from './drift-scan.ts';
9
+ export interface MaintainRuntime {
10
+ /** SkillLibrary-like reader for snapshot assembly. */
11
+ library: SkillLibraryLike;
12
+ /** Subagent spawner (platform `subagents` service — minimal shape for injection). */
13
+ subagents: {
14
+ start(kind: string, options: unknown): Promise<{
15
+ result: Promise<unknown>;
16
+ }>;
17
+ };
18
+ /** Parent agent/session handle passed through to the subagent, when available. */
19
+ parent?: unknown;
20
+ }
21
+ export interface MaintainOptions {
22
+ timeoutMs?: number;
23
+ maxDepth?: number;
24
+ model?: string;
25
+ provider?: string;
26
+ toolAllow?: readonly string[];
27
+ redact?: ((text: string) => string) | undefined;
28
+ supportFiles?: () => ReadonlyMap<string, readonly string[]>;
29
+ descriptions?: () => ReadonlyMap<string, string>;
30
+ quality?: () => ReadonlyMap<string, number>;
31
+ usageObserved?: () => boolean | undefined;
32
+ }
33
+ export interface MaintainOutcome {
34
+ ok: boolean;
35
+ error?: string | undefined;
36
+ runId?: string | undefined;
37
+ verdict?: 'issues' | 'no_issues' | undefined;
38
+ text?: string | undefined;
39
+ forcedHuman?: string[] | undefined;
40
+ }
41
+ /** Render template-M placeholders from the signal vocabulary (011 single-source rule). */
42
+ export declare function renderMaintainTemplate(template: string, bundleVersion: string, signalsVersion: string, signature: string): string;
43
+ /** Facts bundle shared by the full scan and the `--facts` 0-token preview. */
44
+ export interface FactsBundle {
45
+ report: DriftReport;
46
+ facts: string;
47
+ signalsVersion: string;
48
+ signature: string;
49
+ }
50
+ /**
51
+ * Deterministic half of a maintenance scan: snapshots → drift report →
52
+ * rendered facts block (joint signature + redaction). Shared by `runMaintain`
53
+ * and the `--facts` preview so the preview can never disagree with the scan.
54
+ */
55
+ export declare function buildMaintainFacts(snapshots: ReadonlyArray<DriftSkillSnapshot>, usageObserved: boolean | undefined, redact: ((text: string) => string) | undefined): FactsBundle;
56
+ /** Run one maintenance scan and return display text plus validation metadata. */
57
+ export declare function runMaintain(runtime: MaintainRuntime, options?: MaintainOptions): Promise<MaintainOutcome>;
58
+ //# sourceMappingURL=orchestrate.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Probe detail computation (011 Phase 3).
3
+ *
4
+ * Pure functions over a skill snapshot — the SAME calculators the scan uses
5
+ * (`duplicateHeadings` / `overlongLines` / `missingSupportPointers` /
6
+ * `narrowNameMatches` / `computeDedupGroups` / `computePrefixClusters`), so a
7
+ * probe result can never disagree with the facts block. No IO, no writes.
8
+ */
9
+ import { type DriftSkillSnapshot } from '@deepseek-ai/dsh-evolution-core';
10
+ export declare const PROBE_SIGNALS: ReadonlyArray<string>;
11
+ export interface ProbeResult {
12
+ signal: string;
13
+ /** Present only when the query carried a target. */
14
+ target?: string;
15
+ detail: string[];
16
+ }
17
+ /**
18
+ * Compute probe details for a query. Skill-level signals require `target`;
19
+ * library-level signals ignore it. Unknown signals yield an explicit
20
+ * `unknown-signal` detail (never a fabricated verdict).
21
+ */
22
+ export declare function computeProbe(signal: string, target: string | undefined, snapshots: ReadonlyArray<DriftSkillSnapshot>): ProbeResult;
23
+ //# sourceMappingURL=probe.d.ts.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Mechanical-facts rendering for the maintenance subagent (011 §6).
3
+ *
4
+ * One canonical block: opening tag with version + joint signature, one
5
+ * `[FACT]`/`[UNKNOWN]` line per signal, closing tag. The template side
6
+ * renders its own `<MAINTAIN_PROMPT>` header with the same signature, so
7
+ * the model can compare the two heads (011 mismatch protocol). All rendered
8
+ * value/detail text passes through the optional redactor before it leaves
9
+ * the session.
10
+ */
11
+ import { type DriftReport, type DriftSkillAssessment } from '@deepseek-ai/dsh-evolution-core';
12
+ export interface RenderFactsOptions {
13
+ /** signals_version — changes whenever the signal set/thresholds change. */
14
+ signalsVersion: string;
15
+ /** Joint signature: sha256(template-text + signal-definitions) — shared with MAINTAIN_PROMPT head. */
16
+ signature: string;
17
+ /** Optional redactor; defaults to core `redactSecrets`. */
18
+ redact?: ((text: string) => string) | undefined;
19
+ }
20
+ export declare function renderFacts(report: DriftReport, options: RenderFactsOptions): string;
21
+ /** Convenience for tests and replays: summarize a skill assessment. */
22
+ export declare function summarizeAssessment(assessment: DriftSkillAssessment): string;
23
+ //# sourceMappingURL=render-facts.d.ts.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `maintenance_probe` tool (011 Phase 3).
3
+ *
4
+ * Read-only deep-dive: returns single-source machine detail for one signal
5
+ * (library-level group/cluster membership, or per-skill line/pointer/shape
6
+ * evidence). The subagent may use it to sharpen confidence and
7
+ * semantic_reasoning; it must never introduce evidence ids outside the facts
8
+ * block (validated plan-side). No write path exists in this tool.
9
+ * @module @deepseek-ai/dsh-evolution-maintenance-tools
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ export declare const name = "evolution-maintenance-tools";
13
+ export interface Config {
14
+ /** Skill-tree root for probe reads; empty uses skillsRoot(). Align with
15
+ * tool-skill-manage/skill-usage/evolution-skill-catalog/commands rows (A7). */
16
+ skillsRoot?: string | undefined;
17
+ }
18
+ export declare function apply(ctx: Context, rawConfig?: Config): void;
19
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Maintain-plan validation and normalization (011 §7).
3
+ *
4
+ * Contract checks (mechanical, no semantics): enum membership, required
5
+ * fields, evidence closed over the facts block's signal set, needs_human
6
+ * consistency with the confidence/reversibility/is_override/unknown rules,
7
+ * and the quality_low=unknown global imposition. Never judges whether a
8
+ * recommendation is right — only whether it is well-formed and traceable.
9
+ */
10
+ import { type DriftReport } from '@deepseek-ai/dsh-evolution-core';
11
+ export type MaintainVerdict = 'issues' | 'no_issues';
12
+ export type MaintainPlanItemKind = 'skill-level' | 'relationship-level' | 'library-level';
13
+ export type MaintainReversibility = 'archive' | 'restructure' | 'patch' | 'rename' | 'none';
14
+ export type MaintainImpact = 'better' | 'worse' | 'neutral';
15
+ export interface MaintainEvidence {
16
+ signal: string;
17
+ value: string;
18
+ }
19
+ export interface MaintainPlanItem {
20
+ kind: MaintainPlanItemKind;
21
+ names: string[];
22
+ rule: string;
23
+ evidence: MaintainEvidence[];
24
+ finding: string;
25
+ recommendation: string;
26
+ semantic_reasoning: string;
27
+ impact: MaintainImpact;
28
+ impact_reason: string;
29
+ reversibility: MaintainReversibility;
30
+ undo_path: string;
31
+ confidence: number;
32
+ needs_human: boolean;
33
+ is_override: boolean;
34
+ override_reason?: string | undefined;
35
+ }
36
+ export interface MaintainPlan {
37
+ verdict: MaintainVerdict;
38
+ plan: MaintainPlanItem[];
39
+ notes: string[];
40
+ }
41
+ export interface ValidationResult {
42
+ ok: boolean;
43
+ errors: string[];
44
+ /** Normalized plan (needs_human impositions applied). */
45
+ plan: MaintainPlan;
46
+ /** Skill names whose items were force-marked needs_human by the quality_low gate. */
47
+ forcedHuman: string[];
48
+ }
49
+ /**
50
+ * Validate and normalize a maintain plan against the facts report.
51
+ * @param raw - subagent output (already JSON-parsed by the subagent channel).
52
+ * @param report - the DriftReport rendered into the facts block (quality_low gate source).
53
+ * @param validSignals - ids present in the rendered facts block.
54
+ */
55
+ export declare function validateAndNormalizeMaintainPlan(raw: unknown, report: DriftReport, validSignals: ReadonlySet<string>): ValidationResult;
56
+ //# sourceMappingURL=validate-plan.d.ts.map
package/package.json CHANGED
@@ -1 +1,52 @@
1
- {"name":"@lmzhen/dsh-evolution-maintenance","version":"0.0.0","description":"placeholder bootstrap for OIDC publisher setup","license":"MIT"}
1
+ {
2
+ "name": "@lmzhen/dsh-evolution-maintenance",
3
+ "description": "Deterministic maintenance-scan surface: skill-library snapshot assembly, drift signals and mechanical-facts rendering (design 011) (community build)",
4
+ "version": "0.3.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/lmzhen/dsh-evolution.git",
11
+ "directory": "packages/dsh-evolution-maintenance"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./tools": {
26
+ "types": "./lib/types/tools.d.ts",
27
+ "default": "./lib/tools.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "lib/index.js",
33
+ "lib/invariant.js",
34
+ "lib/tools.js",
35
+ "lib/types/**/*.d.ts",
36
+ "lib/types/invariant.d.ts",
37
+ "lib/types/tools.d.ts"
38
+ ],
39
+ "license": "MIT",
40
+ "dependencies": {
41
+ "@lmzhen/dsh-evolution-core": "^0.3.0"
42
+ },
43
+ "peerDependencies": {
44
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
45
+ "@deepseek-ai/cordis": "^4.0.1",
46
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
47
+ },
48
+ "devDependencies": {
49
+ "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
50
+ "@deepseek-ai/schemastery": "^3.18.1"
51
+ }
52
+ }