@lmzhen/dsh-evolution-commands 0.16.1 → 0.17.0-rc.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/lib/index.js +728 -188
- package/lib/types/doctor.d.ts +36 -7
- package/lib/types/index.d.ts +21 -13
- package/lib/types/migration.d.ts +104 -0
- package/lib/types/params.d.ts +4 -4
- package/lib/types/platform-view.d.ts +97 -0
- package/lib/types/preset-source.d.ts +44 -0
- package/package.json +8 -8
package/lib/index.js
CHANGED
|
@@ -2,11 +2,296 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
|
|
4
4
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
-
import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS,
|
|
5
|
+
import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, appendEvolutionEvent, applyNamespaceMigration, assertSkillsRootAliasRetired, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionRoot, isDeprecatedParamId, isMissingPath, mergePresetRow, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport } from "@lmzhen/dsh-evolution-core";
|
|
6
6
|
import { buildEnrichment, buildMaintainFacts, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
|
|
7
|
-
import { copyFileSync, existsSync,
|
|
7
|
+
import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
//#region lib/types/migration.js
|
|
11
|
+
/**
|
|
12
|
+
* G3 (caller half): find the legacy settings document and run the namespace migration.
|
|
13
|
+
*
|
|
14
|
+
* The platform's own one-time import knows only its private table of entries, so a family
|
|
15
|
+
* section left in the document only produces a warn there — the values stay in the file the
|
|
16
|
+
* import renamed. This module reads that file (the renamed one first, a not-yet-imported
|
|
17
|
+
* `settings.yaml` second), plans with core's pure planner, and merges through the platform's
|
|
18
|
+
* own write. Everything it touches is injected, so a spec drives it without a host.
|
|
19
|
+
* @module @lmzhen/dsh-evolution-commands/migration
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The legacy document names the platform leaves behind, in read order.
|
|
23
|
+
* @param home - the profile home the document sits in.
|
|
24
|
+
* @returns both candidate paths, the renamed import first.
|
|
25
|
+
*/
|
|
26
|
+
function legacySettingsPaths(home) {
|
|
27
|
+
return [join(home, "settings.yaml.imported"), join(home, "settings.yaml")];
|
|
28
|
+
}
|
|
29
|
+
/** Nothing to do: no document, or no settings seat to write through. */
|
|
30
|
+
const NOTHING = {
|
|
31
|
+
source: void 0,
|
|
32
|
+
plan: void 0,
|
|
33
|
+
report: void 0
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Read the first legacy document that exists and merge what it holds.
|
|
37
|
+
*
|
|
38
|
+
* The write goes through the platform's own `settings.update`, so the merge semantics, the
|
|
39
|
+
* revision fence and the row's schema validation are the platform's — a row whose schema
|
|
40
|
+
* refuses a key refuses the whole patch, which is why the planner refuses unknown keys
|
|
41
|
+
* before this runs.
|
|
42
|
+
* @param run - the home, the reader, the settings seat and the warning sink.
|
|
43
|
+
* @returns the source file, its plan, and what the run did (all undefined when there was
|
|
44
|
+
* nothing to read or nowhere to write).
|
|
45
|
+
*/
|
|
46
|
+
async function runNamespaceMigration(run) {
|
|
47
|
+
const settings = run.settings;
|
|
48
|
+
if (settings === void 0) return NOTHING;
|
|
49
|
+
const read = (rowId) => settings.describe?.({ redactSecrets: false }).find((entry) => entry.ns === rowId);
|
|
50
|
+
const update = async (rowId, patch) => {
|
|
51
|
+
if (settings.update === void 0) return;
|
|
52
|
+
await settings.update(rowId, patch);
|
|
53
|
+
};
|
|
54
|
+
if (settings.describe === void 0 || settings.update === void 0) return NOTHING;
|
|
55
|
+
for (const source of legacySettingsPaths(run.home)) {
|
|
56
|
+
const text = await run.io.readText(source);
|
|
57
|
+
if (text === null) continue;
|
|
58
|
+
const plan = planNamespaceMigration(text);
|
|
59
|
+
if (plan.entries.length === 0) return {
|
|
60
|
+
source,
|
|
61
|
+
plan,
|
|
62
|
+
report: void 0
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
source,
|
|
66
|
+
plan,
|
|
67
|
+
report: await applyNamespaceMigration(plan, {
|
|
68
|
+
read,
|
|
69
|
+
update,
|
|
70
|
+
warn: run.warn
|
|
71
|
+
})
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return NOTHING;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Read the legacy document and report what is still unmigrated.
|
|
78
|
+
*
|
|
79
|
+
* Read-only by construction: the answer comes from the pure classifier, never from the
|
|
80
|
+
* applier, so a doctor run cannot write. `runNamespaceMigration` and this read the SAME
|
|
81
|
+
* decision, which is what keeps the report and the next run's outcome from disagreeing.
|
|
82
|
+
* @param run - the home, the reader, the settings seat.
|
|
83
|
+
* @returns the state, or undefined when there is no legacy document or no settings seat.
|
|
84
|
+
*/
|
|
85
|
+
async function readLegacyDocumentState(run) {
|
|
86
|
+
const settings = run.settings;
|
|
87
|
+
if (settings?.describe === void 0) return void 0;
|
|
88
|
+
for (const source of legacySettingsPaths(run.home)) {
|
|
89
|
+
const text = await run.io.readText(source);
|
|
90
|
+
if (text === null) continue;
|
|
91
|
+
const plan = planNamespaceMigration(text);
|
|
92
|
+
const read = (rowId) => settings.describe?.({ redactSecrets: false }).find((entry) => entry.ns === rowId);
|
|
93
|
+
const decided = classifyNamespaceMigration(plan, read);
|
|
94
|
+
return {
|
|
95
|
+
source,
|
|
96
|
+
sections: plan.entries.length,
|
|
97
|
+
pending: decided.write.flatMap((row) => Object.keys(row.patch).map((key) => row.rowId + "." + key)),
|
|
98
|
+
settled: [...decided.unchanged.map((entry) => entry.rowId + "." + entry.key), ...decided.conflicts.map((entry) => entry.rowId + "." + entry.key)]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Run the migration from a plugin context: probe the settings seat, the profile home and the
|
|
104
|
+
* IO seam, then delegate to {@link runNamespaceMigration}.
|
|
105
|
+
*
|
|
106
|
+
* The command reports `problem` as its own error text; a boot-time caller skips instead — the
|
|
107
|
+
* point of naming the missing prerequisite rather than throwing is that both readers can act.
|
|
108
|
+
* @param host - the context (services + logger).
|
|
109
|
+
* @returns the outcome, or the prerequisite that is absent.
|
|
110
|
+
*/
|
|
111
|
+
async function migrateFromContext(host) {
|
|
112
|
+
const settings = host.get("settings");
|
|
113
|
+
if (settings?.update === void 0 || settings.describe === void 0) return {
|
|
114
|
+
ok: false,
|
|
115
|
+
problem: "settings"
|
|
116
|
+
};
|
|
117
|
+
const profile = host.get("profileContext");
|
|
118
|
+
const home = typeof profile?.home === "string" ? profile.home : "";
|
|
119
|
+
if (home === "") return {
|
|
120
|
+
ok: false,
|
|
121
|
+
problem: "home"
|
|
122
|
+
};
|
|
123
|
+
const io = host.get("evolutionIo")?.provider?.();
|
|
124
|
+
if (io === void 0) return {
|
|
125
|
+
ok: false,
|
|
126
|
+
problem: "io"
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
outcome: await runNamespaceMigration({
|
|
131
|
+
home,
|
|
132
|
+
io,
|
|
133
|
+
settings,
|
|
134
|
+
warn: (message) => {
|
|
135
|
+
host.logger.warn(message);
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Render one outcome for the command surface.
|
|
142
|
+
* @param outcome - the run's result.
|
|
143
|
+
* @returns the report text (Chinese copy, the family's user-facing language).
|
|
144
|
+
*/
|
|
145
|
+
function renderNamespaceMigration(outcome) {
|
|
146
|
+
if (outcome.source === void 0) return "迁移:没有找到旧设置文档(settings.yaml.imported / settings.yaml),无需迁移。";
|
|
147
|
+
const report = outcome.report;
|
|
148
|
+
if (report === void 0) return "迁移:" + outcome.source + " 里没有家族的旧分区,无需迁移。";
|
|
149
|
+
const rows = report.written.map((row) => `${row.rowId}:${row.keys.join("/")}`).join(";");
|
|
150
|
+
const kept = report.conflicts.map((conflict) => `${conflict.rowId}.${conflict.key}`).join("、");
|
|
151
|
+
const skipped = report.skipped.map((skip) => `${skip.namespace}.${skip.key}`).join("、");
|
|
152
|
+
return [
|
|
153
|
+
`迁移源:${outcome.source}`,
|
|
154
|
+
`写入:${report.written.length} 行${rows === "" ? "" : `(${rows})`}`,
|
|
155
|
+
`已一致:${report.unchanged.length} 个键(值相同,未写入)`,
|
|
156
|
+
`保留现值:${report.conflicts.length} 个键${kept === "" ? "" : `(${kept})`}`,
|
|
157
|
+
`未迁移:${report.skipped.length} 个键${skipped === "" ? "" : `(${skipped})`}`
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region lib/types/platform-view.js
|
|
162
|
+
/**
|
|
163
|
+
* G5: the platform's own view of the RUNNING profile, read through the services the platform
|
|
164
|
+
* mounts — `pluginManager` (bundles, plugin rows), `configEditor` (inherited vs override) and
|
|
165
|
+
* `pluginInventory` (live rows, agent presets).
|
|
166
|
+
*
|
|
167
|
+
* Read structurally on purpose: the family declares no dependency edge into the platform's
|
|
168
|
+
* management packages, because a plane that does not mount them must render a named line, not
|
|
169
|
+
* fail to load. Every reader answers `undefined` for an absent surface; the caller says what
|
|
170
|
+
* that means, and no caller may re-derive the same facts by scanning the profile off disk — one
|
|
171
|
+
* fact has one home.
|
|
172
|
+
* @module @lmzhen/dsh-evolution-commands/platform-view
|
|
173
|
+
*/
|
|
174
|
+
/**
|
|
175
|
+
* Call one surface, awaiting it, and require an array back.
|
|
176
|
+
* @param read - the call, or undefined when the member is absent.
|
|
177
|
+
* @returns the array, or undefined when the member is absent, threw, or answered something else.
|
|
178
|
+
*/
|
|
179
|
+
async function arrayOf(read) {
|
|
180
|
+
if (read === void 0) return void 0;
|
|
181
|
+
try {
|
|
182
|
+
const value = await read();
|
|
183
|
+
return Array.isArray(value) ? value : void 0;
|
|
184
|
+
} catch {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Call one surface, awaiting it, and require an object back (the inventory answers one).
|
|
190
|
+
* @param read - the call, or undefined when the member is absent.
|
|
191
|
+
* @returns the object, or undefined when the member is absent, threw, or answered an array.
|
|
192
|
+
*/
|
|
193
|
+
async function objectOf(read) {
|
|
194
|
+
if (read === void 0) return void 0;
|
|
195
|
+
try {
|
|
196
|
+
const value = await read();
|
|
197
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
198
|
+
} catch {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Read every platform surface the doctor uses.
|
|
204
|
+
* @param ctx - the context (services), read structurally.
|
|
205
|
+
* @returns the view; each member is undefined when its surface is not mounted or did not answer.
|
|
206
|
+
*/
|
|
207
|
+
async function readPlatformView(ctx) {
|
|
208
|
+
const manager = ctx.get("pluginManager");
|
|
209
|
+
const editor = ctx.get("configEditor");
|
|
210
|
+
const inventory = ctx.get("pluginInventory");
|
|
211
|
+
const listed = await objectOf(inventory?.list === void 0 ? void 0 : () => inventory.list?.());
|
|
212
|
+
const plugins = await arrayOf(manager?.listPlugins === void 0 ? void 0 : () => manager.listPlugins?.());
|
|
213
|
+
return {
|
|
214
|
+
bundles: await arrayOf(manager?.listBundles === void 0 ? void 0 : () => manager.listBundles?.()),
|
|
215
|
+
plugins: plugins ?? listed?.entries,
|
|
216
|
+
configuration: await arrayOf(editor?.configuration === void 0 ? void 0 : () => editor.configuration?.()),
|
|
217
|
+
presets: listed?.agentPresets
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region lib/types/preset-source.js
|
|
222
|
+
/**
|
|
223
|
+
* Where a family preset ROW is written, and where the platform base rows it composes come from.
|
|
224
|
+
*
|
|
225
|
+
* 0.2.x replaced the preset directory with an ordinary composition row
|
|
226
|
+
* (`packages/preset/agent-preset/src/index.ts`), so a family preset is a row in the profile's own
|
|
227
|
+
* patch layer — the file the platform's Web editor also saves preset edits to — and the base rows
|
|
228
|
+
* are the bundle patch layer that declares the platform's preset
|
|
229
|
+
* (`packages/bundle/web-app/presets/<base>.patch.yml`).
|
|
230
|
+
*
|
|
231
|
+
* Platform anchors: `packages/boot/app-boot/src/profile-context.ts` (the launcher-owned
|
|
232
|
+
* `profileContext` — `{ name, dir, patchPath, startedBundles }` — this module reads) and
|
|
233
|
+
* `packages/boot/app-boot/src/profile.ts` (`PROFILE_PATCH_FILENAME`).
|
|
234
|
+
* @module
|
|
235
|
+
*/
|
|
236
|
+
/** The patch file name inside a profile directory (`packages/boot/app-boot/src/profile.ts`,
|
|
237
|
+
* `PROFILE_PATCH_FILENAME`). */
|
|
238
|
+
const PROFILE_PATCH_FILENAME$1 = "cordis.patch.yml";
|
|
239
|
+
/**
|
|
240
|
+
* The profile patch this process writes into, or undefined outside a profile.
|
|
241
|
+
*
|
|
242
|
+
* `profileContext` is registered by the profile launcher only
|
|
243
|
+
* (`packages/boot/app-boot/src/profile-context.ts`); a bare library mount — the family's own specs,
|
|
244
|
+
* or a host started without a profile — has none, and then there is no file for a row to live in.
|
|
245
|
+
* @param ctx - the plugin context (a `get(name)` view is enough).
|
|
246
|
+
* @returns the target, or undefined when no profile is mounted.
|
|
247
|
+
*/
|
|
248
|
+
function presetProfileTarget(ctx) {
|
|
249
|
+
const profile = ctx.get("profileContext");
|
|
250
|
+
if (profile === void 0 || typeof profile.dir !== "string" || profile.dir === "") return void 0;
|
|
251
|
+
return {
|
|
252
|
+
profile: profile.name,
|
|
253
|
+
profileDir: profile.dir,
|
|
254
|
+
patchPath: typeof profile.patchPath === "string" && profile.patchPath !== "" ? profile.patchPath : join(profile.dir, PROFILE_PATCH_FILENAME$1),
|
|
255
|
+
bundles: Array.isArray(profile.startedBundles) ? profile.startedBundles : []
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Resolve the platform base preset patch one `--base` composes.
|
|
260
|
+
*
|
|
261
|
+
* The module graph is probed FIRST: a desktop/Electron host resolves the bundle package inside
|
|
262
|
+
* `app.asar`, where no file walk reaches — the platform packages are not on disk at all. The file
|
|
263
|
+
* candidates (the target profile's own `node_modules`, then the unpacked Electron resource scope,
|
|
264
|
+
* then a source checkout, then a global install, then `DSH_AGENT_PRESET_ROOT`) and the loud failure
|
|
265
|
+
* live in core's `resolveBasePresetPatch`.
|
|
266
|
+
* @param base - the base name from `evolution-agent/bases.json`.
|
|
267
|
+
* @param target - the profile target, when one is mounted.
|
|
268
|
+
* @returns the patch file's absolute path.
|
|
269
|
+
*/
|
|
270
|
+
function resolvePresetBasePatch(base, target) {
|
|
271
|
+
return resolveBasePresetPatch(base, {
|
|
272
|
+
root: process.env["DSH_AGENT_PRESET_ROOT"],
|
|
273
|
+
resolve: moduleGraphPath,
|
|
274
|
+
profileDir: target?.profileDir,
|
|
275
|
+
bundles: target?.bundles,
|
|
276
|
+
fromDir: dirname(fileURLToPath(import.meta.url))
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The module-graph probe used by {@link resolvePresetBasePatch}.
|
|
281
|
+
*
|
|
282
|
+
* `import.meta.resolve` is the only lookup an Electron main process performs asar-aware, so it is
|
|
283
|
+
* the candidate that reaches the platform packages inside `resources/app.asar`. The function is
|
|
284
|
+
* optional in the runtime and the probe is a candidate, not a requirement: core treats a throw as a
|
|
285
|
+
* miss and continues down the file chain.
|
|
286
|
+
* @param specifier - the package-relative specifier to resolve.
|
|
287
|
+
* @returns the resolved file path.
|
|
288
|
+
*/
|
|
289
|
+
function moduleGraphPath(specifier) {
|
|
290
|
+
const resolve = import.meta.resolve;
|
|
291
|
+
if (typeof resolve !== "function") throw new Error("import.meta.resolve is unavailable in this runtime");
|
|
292
|
+
return fileURLToPath(resolve(specifier));
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
10
295
|
//#region lib/types/doctor.js
|
|
11
296
|
/**
|
|
12
297
|
* `/evolution doctor` — read-only self-check (0.3.55, WB2).
|
|
@@ -31,51 +316,112 @@ const EVOLUTION_BUNDLE_TAILS = new Set([
|
|
|
31
316
|
"dsh-evolution-preset"
|
|
32
317
|
]);
|
|
33
318
|
/**
|
|
319
|
+
* G5: which of the three family bundle forms a bundle-name list carries.
|
|
320
|
+
* @param bundles - package names.
|
|
321
|
+
* @returns one flag per form.
|
|
322
|
+
*/
|
|
323
|
+
function familyForms(bundles) {
|
|
324
|
+
return {
|
|
325
|
+
full: bundles.some((name) => tailOf(name) === "dsh-evolution-all"),
|
|
326
|
+
host: bundles.some((name) => tailOf(name) === "dsh-evolution-host"),
|
|
327
|
+
preset: bundles.some((name) => tailOf(name) === "dsh-evolution-preset")
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* G5: the live Loader entry carrying one family row.
|
|
332
|
+
* @param plugins - the platform's entry list.
|
|
333
|
+
* @param rowId - the row id the family knows.
|
|
334
|
+
* @returns the entry, or undefined when this runtime carries no such row.
|
|
335
|
+
*/
|
|
336
|
+
function familyRow(plugins, rowId) {
|
|
337
|
+
return plugins.find((entry) => entry.entryId === rowId || entry.entryId.endsWith(":" + rowId) || entry.moduleName === rowId);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* G5: the platform's own agent-preset verdicts.
|
|
341
|
+
*
|
|
342
|
+
* Only `broken` survives from the two candidates the design named. The other one — a row two
|
|
343
|
+
* presets both declare — was measured on a real deployment and dropped: every platform base
|
|
344
|
+
* composes the SAME base rows, so `persona` / `agent-instructions` / … are declared by every
|
|
345
|
+
* preset in a perfectly healthy install, which buries the one case worth reporting (the family's
|
|
346
|
+
* own bundle-vs-preset double mount, which the conflict matrix below already owns).
|
|
347
|
+
* @param presets - the inventory's preset list, or undefined without that surface.
|
|
348
|
+
* @returns one line per finding.
|
|
349
|
+
*/
|
|
350
|
+
function presetIssues(presets) {
|
|
351
|
+
if (presets === void 0) return [];
|
|
352
|
+
const issues = [];
|
|
353
|
+
for (const preset of presets) if (preset.broken !== void 0) issues.push(`agent preset \`${preset.id}\` is BROKEN (${preset.broken}) — the platform could not compose it`);
|
|
354
|
+
return issues;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
34
357
|
* S4.3: the parameter-surface divergences a RUNNING deployment can show and the
|
|
35
358
|
* build-time guards cannot. Three classes, each with its own move:
|
|
36
359
|
* - a user override (the effective value is no longer the deployment's),
|
|
37
360
|
* - a deprecated alias still written in a user section (writes refuse it),
|
|
38
361
|
* - a declared user-writable parameter whose owner publishes no user layer in
|
|
39
362
|
* this composition (the registry promises a face this deployment never mounts).
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
363
|
+
* G5: the two layers and the live rows come from the platform (`configEditor.configuration()`,
|
|
364
|
+
* `pluginManager.listPlugins()`); the registry side (which spellings are deprecated, which rows
|
|
365
|
+
* declare an E3 parameter) stays the family's own table. A surface that is not mounted is
|
|
366
|
+
* reported AS SUCH: silence would read as "no divergences", the one claim this section checks.
|
|
367
|
+
* @param platform - the platform view.
|
|
43
368
|
* @returns one readable line per divergence, empty when there is nothing to compare.
|
|
44
369
|
*/
|
|
45
|
-
function paramDivergences(
|
|
46
|
-
|
|
47
|
-
if (provider?.describe === void 0) return [];
|
|
48
|
-
let descriptors;
|
|
49
|
-
try {
|
|
50
|
-
descriptors = provider.describe({ redactSecrets: false });
|
|
51
|
-
} catch (error) {
|
|
52
|
-
return [`settings surface unreadable (${error instanceof Error ? error.message : String(error)}) — parameter divergences were NOT checked`];
|
|
53
|
-
}
|
|
370
|
+
function paramDivergences(platform) {
|
|
371
|
+
if (platform.configuration === void 0) return ["the platform configuration surface (configEditor.configuration) is not mounted here — parameter divergences were NOT checked"];
|
|
54
372
|
const issues = [];
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
373
|
+
for (const row of platform.configuration) {
|
|
374
|
+
const override = asMapping(row.override);
|
|
375
|
+
if (override === void 0) continue;
|
|
376
|
+
const keys = Object.keys(override);
|
|
377
|
+
if (keys.length === 0) continue;
|
|
378
|
+
const entryId = row.entry.options?.id ?? row.entry.options?.name ?? "(unnamed entry)";
|
|
379
|
+
const shown = keys.slice(0, 5).map((key) => `${key}=${JSON.stringify(override[key])}`);
|
|
380
|
+
const more = keys.length > shown.length ? ` and ${keys.length - shown.length} more` : "";
|
|
381
|
+
issues.push(`user override: ${entryId} sets ${keys.length} parameter(s) — ${shown.join(", ")}${more}`);
|
|
63
382
|
for (const key of keys) {
|
|
64
383
|
if (!isDeprecatedParamId(key)) continue;
|
|
65
|
-
issues.push(`deprecated name: ${
|
|
384
|
+
issues.push(`deprecated name: ${entryId} still writes "${key}" — write "${resolveParamId(key)}" instead (writes refuse the alias; it is removed in 0.7.0)`);
|
|
66
385
|
}
|
|
67
386
|
}
|
|
387
|
+
if (platform.plugins === void 0) {
|
|
388
|
+
issues.push("the platform plugin-row surface (pluginManager.listPlugins) is not mounted here — declared-but-unreachable parameters were NOT checked");
|
|
389
|
+
return issues;
|
|
390
|
+
}
|
|
391
|
+
const live = liveRowTails(platform.plugins);
|
|
68
392
|
const unreachable = PARAM_EXPOSURE.filter((entry) => entry.tier === "E3").filter((entry) => {
|
|
69
|
-
const namespace =
|
|
70
|
-
return namespace === void 0 || !
|
|
393
|
+
const namespace = paramSettingsId(entry.owner);
|
|
394
|
+
return namespace === void 0 || !live.has(namespace);
|
|
71
395
|
}).map((entry) => entry.id);
|
|
72
396
|
if (unreachable.length > 0) {
|
|
73
397
|
const shown = unreachable.slice(0, 6).join(", ");
|
|
74
398
|
const more = unreachable.length > 6 ? ` (+${unreachable.length - 6} more)` : "";
|
|
75
|
-
issues.push(`declared user-writable but unreachable here: ${shown}${more} — the owning
|
|
399
|
+
issues.push(`declared user-writable but unreachable here: ${shown}${more} — the owning row is not mounted in this runtime`);
|
|
76
400
|
}
|
|
77
401
|
return issues;
|
|
78
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* The row id a Loader entry id ends in (`include:memory-files` → `memory-files`).
|
|
405
|
+
* @param plugins - the platform's entry list.
|
|
406
|
+
* @returns the tails, plus every module name the platform reported.
|
|
407
|
+
*/
|
|
408
|
+
function liveRowTails(plugins) {
|
|
409
|
+
const ids = /* @__PURE__ */ new Set();
|
|
410
|
+
for (const entry of plugins) {
|
|
411
|
+
ids.add(entry.entryId);
|
|
412
|
+
ids.add(entry.entryId.slice(entry.entryId.lastIndexOf(":") + 1));
|
|
413
|
+
if (entry.moduleName !== void 0) ids.add(entry.moduleName);
|
|
414
|
+
}
|
|
415
|
+
return ids;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Read a value as a plain mapping (an override layer with no keys is not a divergence).
|
|
419
|
+
* @param value - the parsed layer.
|
|
420
|
+
* @returns the mapping, or undefined for anything else.
|
|
421
|
+
*/
|
|
422
|
+
function asMapping(value) {
|
|
423
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
424
|
+
}
|
|
79
425
|
/** Profile bundle rows for evolution-family packages across all profiles.
|
|
80
426
|
*
|
|
81
427
|
* `onReadError` reports a profile whose manifest could not be READ or PARSED.
|
|
@@ -155,7 +501,8 @@ function memoryInterpolationIssues(home) {
|
|
|
155
501
|
/** G3-② (B2): the family delta container. An optionalDependency of this
|
|
156
502
|
* package, so resolution may legitimately fail; every failure degrades to
|
|
157
503
|
* "unknown" — the doctor never throws.
|
|
158
|
-
* Platform anchor:
|
|
504
|
+
* Platform anchor: `packages/boot/app-boot/src/profile.ts` — a family preset is a ROW in the
|
|
505
|
+
* profile's own patch layer there (`PROFILE_PATCH_FILENAME`), never a preset directory. */
|
|
159
506
|
const AGENT_PRESET_PACKAGE = "@lmzhen/dsh-evolution-agent-preset";
|
|
160
507
|
/** Resolve one asset the package declares in its exports map.
|
|
161
508
|
* @param asset - bases.json or agent.cordis.yml.
|
|
@@ -182,9 +529,18 @@ function readAgentPresetBases$1(path) {
|
|
|
182
529
|
for (const raw of parsed.bases) {
|
|
183
530
|
const entry = raw;
|
|
184
531
|
if (typeof entry.name !== "string" || typeof entry.id !== "string") return null;
|
|
532
|
+
const display = entry.display;
|
|
533
|
+
if (display === null || typeof display !== "object" || typeof display.name !== "string" || typeof display.description !== "string" || typeof display.order !== "number" || !Number.isInteger(display.order)) return null;
|
|
534
|
+
const requires = entry.requires;
|
|
185
535
|
rows.push({
|
|
186
536
|
name: entry.name,
|
|
187
537
|
id: entry.id,
|
|
538
|
+
display: {
|
|
539
|
+
name: display.name,
|
|
540
|
+
description: display.description,
|
|
541
|
+
order: display.order
|
|
542
|
+
},
|
|
543
|
+
...requires !== null && typeof requires === "object" && typeof requires.service === "string" ? { requires: { service: requires.service } } : {},
|
|
188
544
|
...typeof entry.unsupported === "string" ? { unsupported: entry.unsupported } : {}
|
|
189
545
|
});
|
|
190
546
|
}
|
|
@@ -193,67 +549,117 @@ function readAgentPresetBases$1(path) {
|
|
|
193
549
|
return null;
|
|
194
550
|
}
|
|
195
551
|
}
|
|
196
|
-
/**
|
|
197
|
-
*
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
552
|
+
/** The profile patch file name (`packages/boot/app-boot/src/profile.ts`,
|
|
553
|
+
* `PROFILE_PATCH_FILENAME`). */
|
|
554
|
+
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
555
|
+
/** Every profile directory under one DSH_HOME, by name.
|
|
556
|
+
* @param home - resolved DSH_HOME.
|
|
557
|
+
* @returns `{ profile, dir }` per directory under `profiles/` that carries a manifest.
|
|
558
|
+
*/
|
|
559
|
+
function profileDirectories(home) {
|
|
560
|
+
const out = [];
|
|
561
|
+
const root = join(home, "profiles");
|
|
202
562
|
let entries;
|
|
203
563
|
try {
|
|
204
564
|
entries = readdirSync(root, { withFileTypes: true });
|
|
205
565
|
} catch {
|
|
206
|
-
return
|
|
566
|
+
return out;
|
|
207
567
|
}
|
|
208
|
-
for (const entry of entries)
|
|
209
|
-
|
|
568
|
+
for (const entry of entries) {
|
|
569
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
570
|
+
if (entry.name === "node_modules") continue;
|
|
571
|
+
const dir = join(root, entry.name);
|
|
572
|
+
if (!existsSync(join(dir, "package.json"))) continue;
|
|
573
|
+
out.push({
|
|
574
|
+
profile: entry.name,
|
|
575
|
+
dir
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return out;
|
|
579
|
+
}
|
|
580
|
+
/** The bundle rows one profile manifest mounts (scope-agnostic, as written).
|
|
581
|
+
* @param profileDir - the profile directory.
|
|
582
|
+
* @returns the bundle names; empty when the manifest is missing or torn.
|
|
583
|
+
*/
|
|
584
|
+
function profileBundles(profileDir) {
|
|
585
|
+
try {
|
|
586
|
+
const bundles = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")).dsh?.profile?.bundles;
|
|
587
|
+
return Array.isArray(bundles) ? bundles.filter((name) => typeof name === "string") : [];
|
|
588
|
+
} catch {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
/** The profile patch text, or `''` when the profile has none.
|
|
593
|
+
* @param profileDir - the profile directory.
|
|
594
|
+
* @returns the patch text.
|
|
595
|
+
*/
|
|
596
|
+
function profilePatchText(profileDir) {
|
|
597
|
+
try {
|
|
598
|
+
return readFileSync(join(profileDir, PROFILE_PATCH_FILENAME), "utf8");
|
|
599
|
+
} catch {
|
|
600
|
+
return "";
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
/** Whether a base's `requires.service` precondition is met by one profile's bundles — the same
|
|
604
|
+
* judgement the installer applies (`baseUnavailableReason`), read from the same rows.
|
|
605
|
+
* @param base - the table row.
|
|
606
|
+
* @param bundles - the profile's bundle names.
|
|
607
|
+
* @returns true when the base can be installed here.
|
|
608
|
+
*/
|
|
609
|
+
function baseInstallable(base, bundles) {
|
|
610
|
+
if (typeof base.unsupported === "string") return false;
|
|
611
|
+
const service = base.requires?.service;
|
|
612
|
+
if (typeof service !== "string" || service === "") return true;
|
|
613
|
+
return bundles.some((name) => name.trim().endsWith("dsh-web-app"));
|
|
210
614
|
}
|
|
211
615
|
/**
|
|
212
|
-
* S1-F1: ids of the family's
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
616
|
+
* S1-F1: ids of the family's INSTALLED preset rows — every declared preset row in a profile patch
|
|
617
|
+
* that can actually carry the family model rows.
|
|
618
|
+
*
|
|
619
|
+
* The artifact is a declared preset ROW in a profile's own patch layer, so this probe reads that
|
|
620
|
+
* file (`PROFILE_PATCH_FILENAME`) in every profile of the home. A probe narrowed to one base id
|
|
621
|
+
* leaves a `--base ptc|cordis` install invisible: installForm then degrades to `none`/`host` and
|
|
622
|
+
* the action ladder recommends installing `evolution-all` on top — the exact double-mount the
|
|
623
|
+
* doctor exists to prevent.
|
|
219
624
|
*
|
|
220
|
-
* A
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* @
|
|
226
|
-
* @returns installed family base ids whose `agent.cordis.yml` is on disk.
|
|
625
|
+
* A row counts only when the base table names its id as a SUPPORTED family base (the `unsupported`
|
|
626
|
+
* minimal base carries no model rows and cannot double-mount anything). When the table itself
|
|
627
|
+
* cannot be read, any `preset-<family stem>*` row counts — the flag-rather-than-stay-silent
|
|
628
|
+
* posture of {@link probePresetFreshness}.
|
|
629
|
+
* @param home - resolved DSH_HOME.
|
|
630
|
+
* @returns installed family base ids, in profile-then-table order.
|
|
227
631
|
*/
|
|
228
|
-
function installedLayeredPresetIds(
|
|
229
|
-
const delivered = installedPresetIds(root).filter((id) => existsSync(join(root, id, "agent.cordis.yml")));
|
|
230
|
-
if (delivered.length === 0) return [];
|
|
632
|
+
function installedLayeredPresetIds(home) {
|
|
231
633
|
const basesPath = resolveAgentPresetAsset("bases.json");
|
|
232
634
|
const bases = basesPath === null ? null : readAgentPresetBases$1(basesPath);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
635
|
+
const ids = /* @__PURE__ */ new Set();
|
|
636
|
+
for (const { dir } of profileDirectories(home)) {
|
|
637
|
+
const text = profilePatchText(dir);
|
|
638
|
+
if (text === "") continue;
|
|
639
|
+
if (bases === null) {
|
|
640
|
+
for (const match of text.matchAll(/^\s*- id:\s*(preset-[a-z0-9-]+)\s*$/gm)) if (match[1]?.startsWith(`preset-evolution`)) ids.add(match[1].slice(7));
|
|
641
|
+
continue;
|
|
238
642
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
643
|
+
for (const base of bases) {
|
|
644
|
+
if (typeof base.unsupported === "string") continue;
|
|
645
|
+
if (presetRowBlock(text, presetRowId(base.id)) !== null) ids.add(base.id);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return [...ids];
|
|
242
649
|
}
|
|
243
650
|
/**
|
|
244
651
|
* G3-② (B2): recompute what a fresh generation would write for every
|
|
245
652
|
* installed variant and compare it byte-for-byte with the file on disk.
|
|
246
653
|
*
|
|
247
|
-
* The recomputation goes through
|
|
248
|
-
* /evolution preset install and install-layered.mjs apply (
|
|
249
|
-
* row-overrides.json), so doctor cannot disagree with the
|
|
250
|
-
* "fresh" means. Read-only by contract: the snapshot is
|
|
654
|
+
* The recomputation goes through composePresetEntry — the SAME composer
|
|
655
|
+
* /evolution preset install and install-layered.mjs apply (base patch rows +
|
|
656
|
+
* collision guard + row-overrides.json), so doctor cannot disagree with the
|
|
657
|
+
* installer about what "fresh" means. Read-only by contract: the snapshot is
|
|
658
|
+
* never rewritten.
|
|
251
659
|
* @param home - resolved DSH_HOME.
|
|
252
|
-
* @
|
|
253
|
-
* @returns one row per base in the family bases.json.
|
|
660
|
+
* @returns one row per profile × installable base of the family bases.json.
|
|
254
661
|
*/
|
|
255
|
-
|
|
256
|
-
const presetsRoot = join(home, ".agent-presets");
|
|
662
|
+
function probePresetFreshness(home) {
|
|
257
663
|
const basesPath = resolveAgentPresetAsset("bases.json");
|
|
258
664
|
const deltaPath = resolveAgentPresetAsset("agent.cordis.yml");
|
|
259
665
|
const bases = basesPath === null ? null : readAgentPresetBases$1(basesPath);
|
|
@@ -264,86 +670,79 @@ async function probePresetFreshness(home, registry) {
|
|
|
264
670
|
} catch (error) {
|
|
265
671
|
deltaFailure = `${deltaPath} could not be read (${error instanceof Error ? error.message : String(error)})`;
|
|
266
672
|
}
|
|
673
|
+
const profiles = profileDirectories(home);
|
|
267
674
|
if (bases === null) {
|
|
268
675
|
const tablePath = basesPath ?? `${AGENT_PRESET_PACKAGE}/bases.json`;
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
676
|
+
return profiles.flatMap(({ profile, dir }) => {
|
|
677
|
+
const patchPath = join(dir, PROFILE_PATCH_FILENAME);
|
|
678
|
+
return [...profilePatchText(dir).matchAll(/^\s*- id:\s*(preset-[a-z0-9-]+)\s*$/gm)].map((match) => match[1]).map((id) => ({
|
|
679
|
+
profile,
|
|
680
|
+
base: id.slice(7),
|
|
681
|
+
destination: patchPath,
|
|
682
|
+
status: "unknown",
|
|
683
|
+
detail: `the family base table could not be read (${tablePath}) — a fresh generation cannot be recomputed`
|
|
684
|
+
}));
|
|
685
|
+
});
|
|
275
686
|
}
|
|
276
687
|
const rows = [];
|
|
277
|
-
for (const
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
let onDisk;
|
|
331
|
-
try {
|
|
332
|
-
onDisk = readFileSync(compositionPath, "utf8");
|
|
333
|
-
} catch (error) {
|
|
688
|
+
for (const { profile, dir } of profiles) {
|
|
689
|
+
const patchPath = join(dir, PROFILE_PATCH_FILENAME);
|
|
690
|
+
const text = profilePatchText(dir);
|
|
691
|
+
const bundles = profileBundles(dir);
|
|
692
|
+
for (const base of bases) {
|
|
693
|
+
if (!baseInstallable(base, bundles)) continue;
|
|
694
|
+
const block = presetRowBlock(text, presetRowId(base.id));
|
|
695
|
+
if (block === null) {
|
|
696
|
+
rows.push({
|
|
697
|
+
profile,
|
|
698
|
+
base: base.name,
|
|
699
|
+
destination: patchPath,
|
|
700
|
+
status: "absent"
|
|
701
|
+
});
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (delta === null) {
|
|
705
|
+
rows.push({
|
|
706
|
+
profile,
|
|
707
|
+
base: base.name,
|
|
708
|
+
destination: patchPath,
|
|
709
|
+
status: "unknown",
|
|
710
|
+
detail: deltaFailure
|
|
711
|
+
});
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const target = {
|
|
715
|
+
profile,
|
|
716
|
+
profileDir: dir,
|
|
717
|
+
patchPath,
|
|
718
|
+
bundles
|
|
719
|
+
};
|
|
720
|
+
let fresh;
|
|
721
|
+
try {
|
|
722
|
+
fresh = composePresetEntry(readFileSync(resolvePresetBasePatch(base.name, target), "utf8"), delta, {
|
|
723
|
+
rowId: presetRowId(base.id),
|
|
724
|
+
id: base.id,
|
|
725
|
+
name: base.display.name,
|
|
726
|
+
description: base.display.description,
|
|
727
|
+
order: base.display.order
|
|
728
|
+
});
|
|
729
|
+
} catch (error) {
|
|
730
|
+
rows.push({
|
|
731
|
+
profile,
|
|
732
|
+
base: base.name,
|
|
733
|
+
destination: patchPath,
|
|
734
|
+
status: "unknown",
|
|
735
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
736
|
+
});
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
334
739
|
rows.push({
|
|
740
|
+
profile,
|
|
335
741
|
base: base.name,
|
|
336
|
-
destination,
|
|
337
|
-
status: "
|
|
338
|
-
detail: `${compositionPath} could not be read (${error instanceof Error ? error.message : String(error)})`
|
|
742
|
+
destination: patchPath,
|
|
743
|
+
status: presetRowBody(fresh) === presetRowBody(block) ? "fresh" : "differs"
|
|
339
744
|
});
|
|
340
|
-
continue;
|
|
341
745
|
}
|
|
342
|
-
rows.push({
|
|
343
|
-
base: base.name,
|
|
344
|
-
destination,
|
|
345
|
-
status: fresh === onDisk ? "fresh" : "differs"
|
|
346
|
-
});
|
|
347
746
|
}
|
|
348
747
|
return rows;
|
|
349
748
|
}
|
|
@@ -352,6 +751,54 @@ async function probePresetFreshness(home, registry) {
|
|
|
352
751
|
* only `get` is enough); `home` defaults to the evolution root so tests can
|
|
353
752
|
* point doctor at a temp DSH_HOME.
|
|
354
753
|
*/
|
|
754
|
+
/**
|
|
755
|
+
* The legacy settings document's remaining work, for the doctor report.
|
|
756
|
+
*
|
|
757
|
+
* Every failure mode is a verdict, not an exception: a deployment without the settings seat
|
|
758
|
+
* or the family IO seam reports `unavailable`, and an unreadable document is reported through
|
|
759
|
+
* the same warn channel the boot migration uses.
|
|
760
|
+
* @param ctx - the context (services), read structurally.
|
|
761
|
+
* @param home - the profile home the legacy document sits in.
|
|
762
|
+
* @returns the row the report renders.
|
|
763
|
+
*/
|
|
764
|
+
async function legacyMigrationRow(ctx, home) {
|
|
765
|
+
const settings = ctx.get("settings");
|
|
766
|
+
const io = ctx.get("evolutionIo")?.provider?.();
|
|
767
|
+
if (settings?.describe === void 0 || io === void 0) return {
|
|
768
|
+
state: "unavailable",
|
|
769
|
+
source: void 0,
|
|
770
|
+
pending: []
|
|
771
|
+
};
|
|
772
|
+
try {
|
|
773
|
+
const state = await readLegacyDocumentState({
|
|
774
|
+
home,
|
|
775
|
+
io,
|
|
776
|
+
settings
|
|
777
|
+
});
|
|
778
|
+
if (state === void 0) return {
|
|
779
|
+
state: "absent",
|
|
780
|
+
source: void 0,
|
|
781
|
+
pending: []
|
|
782
|
+
};
|
|
783
|
+
if (state.sections === 0) return {
|
|
784
|
+
state: "none",
|
|
785
|
+
source: state.source,
|
|
786
|
+
pending: []
|
|
787
|
+
};
|
|
788
|
+
return {
|
|
789
|
+
state: state.pending.length > 0 ? "pending" : "migrated",
|
|
790
|
+
source: state.source,
|
|
791
|
+
pending: state.pending
|
|
792
|
+
};
|
|
793
|
+
} catch (error) {
|
|
794
|
+
ctx.logger?.warn("evolution-commands: the legacy settings document could not be read — %s", error instanceof Error ? error.message : String(error));
|
|
795
|
+
return {
|
|
796
|
+
state: "unavailable",
|
|
797
|
+
source: void 0,
|
|
798
|
+
pending: []
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
}
|
|
355
802
|
async function diagnose(ctx, options = {}) {
|
|
356
803
|
const home = options.home ?? evolutionRoot();
|
|
357
804
|
const conflicts = [];
|
|
@@ -360,11 +807,18 @@ async function diagnose(ctx, options = {}) {
|
|
|
360
807
|
conflicts.push(scope === "profiles-dir" ? `the home profiles directory could not be enumerated (${detail}) — bundle-based install-form and conflict checks are DEGRADED and may miss installed bundles` : `a profile manifest could not be read or parsed (${detail}) — this profile's bundle rows are UNKNOWN, so the install-form and conflict checks are DEGRADED and may miss installed bundles`);
|
|
361
808
|
});
|
|
362
809
|
const has = (name) => ctx.get(name) !== void 0;
|
|
363
|
-
const presetFreshness =
|
|
364
|
-
const
|
|
365
|
-
const
|
|
366
|
-
const
|
|
367
|
-
const
|
|
810
|
+
const presetFreshness = probePresetFreshness(home);
|
|
811
|
+
const platform = await readPlatformView(ctx);
|
|
812
|
+
const familyBundleRows = platform.bundles?.filter((row) => tailOf(row.name).startsWith("dsh-evolution-"));
|
|
813
|
+
const runtimeBundles = familyBundleRows === void 0 ? void 0 : familyBundleRows.filter((row) => row.enabled).map((row) => row.name);
|
|
814
|
+
const dormantBundles = familyBundleRows?.filter((row) => !row.enabled).map((row) => row.name) ?? [];
|
|
815
|
+
const runtimeFlags = runtimeBundles === void 0 ? void 0 : familyForms(runtimeBundles);
|
|
816
|
+
const aggregateFlags = familyForms(bundles);
|
|
817
|
+
const forms = runtimeFlags ?? aggregateFlags;
|
|
818
|
+
const full = forms.full;
|
|
819
|
+
const host = forms.host;
|
|
820
|
+
const preset = forms.preset;
|
|
821
|
+
const layeredIds = installedLayeredPresetIds(home);
|
|
368
822
|
const presetDirInstalled = layeredIds.length > 0;
|
|
369
823
|
const layeredArtifactLabel = presetDirInstalled ? layeredIds.join(", ") : "evolution";
|
|
370
824
|
const layered = host && presetDirInstalled;
|
|
@@ -373,7 +827,8 @@ async function diagnose(ctx, options = {}) {
|
|
|
373
827
|
if (host && preset) conflicts.push("evolution-host and evolution-preset are installed together — the infra rows double-mount. Keep ONE.");
|
|
374
828
|
if (full && presetDirInstalled) conflicts.push(`evolution-all and the layered Evolution preset (${layeredArtifactLabel}) are both present — the four model rows AND the systemPrompt sections double-instance across the profile and preset layers; the preset loader does not fail on it, shadowing semantics take the nearest layer, so an Evolution-preset session silently runs the preset's copies of everything. Keep ONE (use layered without all, or drop the preset).`);
|
|
375
829
|
if (preset && presetDirInstalled) conflicts.push(`evolution-preset and the layered Evolution preset (${layeredArtifactLabel}) are both present — the preset bundle carries the same model rows as all, so the four model rows AND the systemPrompt sections double-instance and shadowing semantics take the nearest layer (the preset loader does not fail on it). Keep ONE (drop the preset bundle, or remove the layered preset).`);
|
|
376
|
-
const
|
|
830
|
+
const reviewRow = platform.plugins === void 0 ? void 0 : familyRow(platform.plugins, "evolution-review");
|
|
831
|
+
const reviewMounted = reviewRow?.enabled ?? (full || host || preset);
|
|
377
832
|
const services = {
|
|
378
833
|
review: reviewMounted,
|
|
379
834
|
curator: has("evolutionCurator"),
|
|
@@ -408,15 +863,19 @@ async function diagnose(ctx, options = {}) {
|
|
|
408
863
|
if (mountConflicts.length > 0) actions.push("Resolve the conflict first: keep exactly one of evolution-all / evolution-host / evolution-preset / layered.");
|
|
409
864
|
else if (undecidable) actions.push("The install form is UNDECIDABLE (see the DEGRADED row above): an unreadable profiles directory or profile manifest hides whatever is installed, so this report must not add or remove a bundle. Fix the reported read failure, then re-run /evolution doctor.");
|
|
410
865
|
else if (installForm === "none") actions.push("Install the default full bundle: dsh plugin --profile web add @lmzhen/dsh-evolution-all");
|
|
411
|
-
else if (installForm === "preset-only") actions.push(`The Evolution preset is
|
|
866
|
+
else if (installForm === "preset-only") actions.push(`The Evolution preset row is installed but no profile mounts an evolution bundle — re-add @lmzhen/dsh-evolution-host for the layered layout (do NOT add all on top of the preset: that double-mounts the model rows), or drop the row (${layeredArtifactLabel}) from the profile patch if the layered layout is no longer wanted.`);
|
|
412
867
|
else if (installForm === "layered") actions.push("Variant form (session opt-in): model tools follow the Evolution preset, and a session on a platform original preset carries no family rows; add @lmzhen/dsh-evolution-all instead if every session should have them.");
|
|
413
868
|
const env = envIssues();
|
|
414
869
|
if (env.length > 0) actions.push("Fix the DSH_EVOLUTION_* variable listed above.");
|
|
415
870
|
if (services.review && !services.curator) actions.push("Curator service is not mounted — automatic curation is off; verify the host/all bundle row set is complete.");
|
|
416
871
|
if (pendingCount === null && services.approval) actions.push("Approval service is mounted but pending listing failed — check the evolution state service.");
|
|
872
|
+
const presetIssueRows = presetIssues(platform.presets);
|
|
873
|
+
if (presetIssueRows.length > 0) actions.push("Resolve the agent-preset finding above: a broken preset composes no session, and a row two presets declare is resolved by layer order (keep the declaration in one preset).");
|
|
874
|
+
const legacy = await legacyMigrationRow(ctx, home);
|
|
875
|
+
if (legacy.state === "pending") actions.push(`The legacy settings document still holds ${legacy.pending.length} family value(s) — /evolution migrate moves them onto their rows (the document itself is left alone, so nothing is lost)`);
|
|
417
876
|
const memoryIssues = memoryInterpolationIssues(home);
|
|
418
877
|
const budgetIssues = memoryBudgetIssues(ctx);
|
|
419
|
-
const paramIssues = paramDivergences(
|
|
878
|
+
const paramIssues = paramDivergences(platform);
|
|
420
879
|
const queryIssues = await sessionQueryIssues(ctx);
|
|
421
880
|
if (queryIssues.length > 0) actions.push("Session search is degraded: isolate the session named above (or wait for the platform fix described in the family maintenance notes) before retrying the same query");
|
|
422
881
|
if (paramIssues.length > 0) actions.push("Review the parameter divergences above: /evolution params shows every row with its source, /evolution policy set writes a user value, and a declared-but-unreachable row needs its owning plugin row mounted in this profile.");
|
|
@@ -426,7 +885,7 @@ async function diagnose(ctx, options = {}) {
|
|
|
426
885
|
if ((executingCount ?? 0) > 0) actions.push(`${executingCount} staged write(s) are EXECUTING (an approve crashed mid-run — or one is still in flight). Inspect with /evolution pending: if you started the approve, verify the landed write and do not reject it; only reject after verifying no write is intended. For a verified orphan (this build: S2-P2-22), /evolution release <id> returns it to the pending window instead.`);
|
|
427
886
|
for (const row of presetFreshness) {
|
|
428
887
|
if (row.status !== "differs") continue;
|
|
429
|
-
actions.push(`Preset
|
|
888
|
+
actions.push(`Preset row for base "${row.base}" in ${row.destination} DIFFERS from a fresh generation — it is an install-time snapshot; re-run the installer (/evolution preset install) to regenerate`);
|
|
430
889
|
}
|
|
431
890
|
return {
|
|
432
891
|
installForm,
|
|
@@ -443,6 +902,12 @@ async function diagnose(ctx, options = {}) {
|
|
|
443
902
|
executingCount,
|
|
444
903
|
presetFreshness,
|
|
445
904
|
scopedProbe,
|
|
905
|
+
legacy,
|
|
906
|
+
runtimeBundles: runtimeBundles ?? null,
|
|
907
|
+
formSource: runtimeFlags === void 0 ? "aggregate" : "platform",
|
|
908
|
+
presetIssues: presetIssueRows,
|
|
909
|
+
dormantBundles,
|
|
910
|
+
serviceSource: reviewRow === void 0 ? "bundles" : "platform",
|
|
446
911
|
actions
|
|
447
912
|
};
|
|
448
913
|
}
|
|
@@ -550,20 +1015,38 @@ function scopedProbeLine(check) {
|
|
|
550
1015
|
const detail = check.verdict === "hit" ? `${check.hits} scoped evaluation(s) carried the family tools` : check.verdict === "never-hit" ? `${check.misses} scoped evaluation(s) resolved false — review injection and skill-usage telemetry are inert for every session observed; HOST-ONLY installs reach this because tool-memory / tool-skill-manage are not dependencies of evolution-host, VARIANT installs only match once a session runs the Evolution preset` : "the gate has not evaluated a session in this process yet — no session event has reached it since startup";
|
|
551
1016
|
return `scoped rows: ${rows}, probe=${check.verdict} (${detail})`;
|
|
552
1017
|
}
|
|
1018
|
+
/**
|
|
1019
|
+
* One line for the legacy settings document's state.
|
|
1020
|
+
* @param row - the report's legacy row.
|
|
1021
|
+
* @returns the rendered line.
|
|
1022
|
+
*/
|
|
1023
|
+
function legacyLine(row) {
|
|
1024
|
+
if (row.state === "unavailable") return "legacy settings: unknown — no settings seat or no family IO seam is mounted here";
|
|
1025
|
+
if (row.state === "absent") return "legacy settings: no settings.yaml / settings.yaml.imported under this home";
|
|
1026
|
+
if (row.state === "none") return `legacy settings: ${row.source ?? "(unknown)"} carries no family section`;
|
|
1027
|
+
if (row.state === "migrated") return `legacy settings: every family value sits on its row (${row.source ?? "(unknown)"})`;
|
|
1028
|
+
const shown = row.pending.slice(0, 6).join(", ");
|
|
1029
|
+
const more = row.pending.length > 6 ? ` (+${row.pending.length - 6} more)` : "";
|
|
1030
|
+
return `legacy settings: ${row.pending.length} family value(s) still on ${row.source ?? "(unknown)"}: ${shown}${more} — /evolution migrate moves them`;
|
|
1031
|
+
}
|
|
553
1032
|
function renderDoctorText(report) {
|
|
554
1033
|
const lines = [
|
|
555
|
-
`Evolution doctor — install form: ${report.installForm} (deployment: ${report.deploymentForm})`,
|
|
1034
|
+
`Evolution doctor — install form: ${report.installForm} (deployment: ${report.deploymentForm}; from ${report.formSource === "platform" ? "this runtime's bundle surface" : "the cross-profile aggregate"})`,
|
|
1035
|
+
report.runtimeBundles === null ? "runtime bundles: unknown — the platform bundle surface (pluginManager.listBundles) is not mounted here" : `runtime bundles (this profile): ${report.runtimeBundles.length > 0 ? report.runtimeBundles.join(", ") : "(none)"}`,
|
|
1036
|
+
report.dormantBundles.length === 0 ? null : `installed but not selected: ${report.dormantBundles.join(", ")} — the Plugins page lists them; this runtime mounts neither`,
|
|
556
1037
|
`bundles (all profiles): ${report.bundles.length > 0 ? report.bundles.join(", ") : "(none)"}`,
|
|
557
|
-
`services: review=${report.services.review} (inferred from bundles, all profiles) curator=${report.services.curator} approval=${report.services.approval} skillUsage=${report.services.skillUsage} io=${report.services.io}`,
|
|
1038
|
+
`services: review=${report.services.review} (${report.serviceSource === "platform" ? "the live row, this runtime" : "inferred from bundles, all profiles"}) curator=${report.services.curator} approval=${report.services.approval} skillUsage=${report.services.skillUsage} io=${report.services.io}`,
|
|
558
1039
|
scopedProbeLine(report.scopedProbe),
|
|
1040
|
+
legacyLine(report.legacy),
|
|
559
1041
|
`pending: ${report.pendingCount === null ? "unknown" : report.pendingCount}`,
|
|
560
1042
|
`executing: ${report.executingCount === null ? "unknown" : report.executingCount}`
|
|
561
1043
|
];
|
|
562
1044
|
for (const row of report.presetFreshness) {
|
|
563
1045
|
if (row.status === "absent") continue;
|
|
564
1046
|
const verdict = row.status === "fresh" ? "fresh — a fresh generation matches this file byte-for-byte" : row.status === "differs" ? "DIFFERS from a fresh generation — it is an install-time snapshot; re-run the installer (/evolution preset install) to regenerate" : `unknown — a fresh generation could not be recomputed (${row.detail ?? "reason not recorded"})`;
|
|
565
|
-
lines.push(`preset: ${row.destination} ${verdict}`);
|
|
1047
|
+
lines.push(`preset: ${row.base} → ${row.destination} ${verdict}`);
|
|
566
1048
|
}
|
|
1049
|
+
if (report.presetIssues.length > 0) lines.push("agent presets:", ...report.presetIssues.map((line) => ` ! ${line}`));
|
|
567
1050
|
if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
|
|
568
1051
|
if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
|
|
569
1052
|
if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
|
|
@@ -571,7 +1054,7 @@ function renderDoctorText(report) {
|
|
|
571
1054
|
if (report.paramIssues.length > 0) lines.push("parameters:", ...report.paramIssues.map((line) => ` ! ${line}`));
|
|
572
1055
|
if (report.queryIssues.length > 0) lines.push("session search:", ...report.queryIssues.map((line) => ` ! ${line}`));
|
|
573
1056
|
if (report.actions.length > 0) lines.push("next steps:", ...report.actions.map((line) => ` → ${line}`));
|
|
574
|
-
return lines.join("\n");
|
|
1057
|
+
return lines.filter((line) => line !== null).join("\n");
|
|
575
1058
|
}
|
|
576
1059
|
//#endregion
|
|
577
1060
|
//#region lib/types/params.js
|
|
@@ -580,10 +1063,10 @@ function renderDoctorText(report) {
|
|
|
580
1063
|
* surface, so one text answers "which parameters exist, who may write them, and
|
|
581
1064
|
* did I already override this one".
|
|
582
1065
|
*
|
|
583
|
-
* The join key is the OWNER PACKAGE:
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
1066
|
+
* The join key is the OWNER PACKAGE: `settings.describe()` keys a section by the Loader
|
|
1067
|
+
* entry id, which is the row id (core's `paramSettingsId` names that mapping), and reports
|
|
1068
|
+
* that section's raw user section (a key's PRESENCE is the override) plus the resolved
|
|
1069
|
+
* value. Everything here is pure; the command handler supplies the data.
|
|
587
1070
|
* @module @lmzhen/dsh-evolution-commands/params
|
|
588
1071
|
*/
|
|
589
1072
|
/**
|
|
@@ -608,7 +1091,7 @@ function paramSurfaceRows(sections, entries = PARAM_EXPOSURE) {
|
|
|
608
1091
|
applies: entry.applies,
|
|
609
1092
|
owner: entry.owner
|
|
610
1093
|
};
|
|
611
|
-
const namespace =
|
|
1094
|
+
const namespace = paramSettingsId(entry.owner);
|
|
612
1095
|
const section = namespace === void 0 ? void 0 : sections.get(namespace);
|
|
613
1096
|
if (section === void 0) return {
|
|
614
1097
|
...base,
|
|
@@ -804,6 +1287,10 @@ const COMMAND_ENTRIES = [
|
|
|
804
1287
|
usage: "maintain [--timeout=<ms> | --facts]",
|
|
805
1288
|
summary: "运行一次维护扫描(--facts 为 0 token 预览)"
|
|
806
1289
|
},
|
|
1290
|
+
{
|
|
1291
|
+
usage: "migrate",
|
|
1292
|
+
summary: "把旧设置文档里家族的旧分区迁进现在的行(幂等;已迁移过就报「已一致」)"
|
|
1293
|
+
},
|
|
807
1294
|
{
|
|
808
1295
|
usage: "policy set <id> <value> [--expect <revision>]",
|
|
809
1296
|
summary: "通过设置服务写入一个你可改的参数 (E3 only; E1/E2 stay in cordis.yml)"
|
|
@@ -814,7 +1301,7 @@ const COMMAND_ENTRIES = [
|
|
|
814
1301
|
},
|
|
815
1302
|
{
|
|
816
1303
|
usage: "preset install [--base <name>[,<name>...]]",
|
|
817
|
-
summary: "
|
|
1304
|
+
summary: "把自进化 Agent 预设**行**写进当前 profile 的 patch 层(`profiles/<p>/cordis.patch.yml`;bases 来自 agent 包的 bases.json)"
|
|
818
1305
|
},
|
|
819
1306
|
{
|
|
820
1307
|
usage: "restructure <name> \"<heading>\" <to_file> [--plan <runId>]",
|
|
@@ -1089,8 +1576,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1089
1576
|
text: buildLearnPrompt(request)
|
|
1090
1577
|
}],
|
|
1091
1578
|
source: {
|
|
1092
|
-
kind: "
|
|
1093
|
-
plugin: "dsh-evolution-commands",
|
|
1579
|
+
kind: "evolution-commands",
|
|
1094
1580
|
form: "notice",
|
|
1095
1581
|
summary: "learn request"
|
|
1096
1582
|
}
|
|
@@ -1234,34 +1720,38 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1234
1720
|
if (entry === void 0) return err(`Unknown agent-preset base "${name}" — ${join(source, "bases.json")} lists ${table.entries.map((candidate) => candidate.name).join(", ")}`);
|
|
1235
1721
|
selected.push(entry);
|
|
1236
1722
|
}
|
|
1237
|
-
const rawBases = JSON.parse(readFileSync(join(source, "bases.json"), "utf8"));
|
|
1238
1723
|
const deltaPath = join(source, "agent.cordis.yml");
|
|
1239
1724
|
if (!existsSync(deltaPath)) return err(`Preset file missing from ${source} — is the dsh-evolution-agent-preset package installed?`);
|
|
1240
|
-
const
|
|
1241
|
-
if (
|
|
1725
|
+
const profile = presetProfileTarget(ctx);
|
|
1726
|
+
if (profile === void 0) return err("No profile is mounted in this process, so there is no profile patch to write the preset row into — run the command in a session started by `dsh --profile <name>`.");
|
|
1727
|
+
const delta = readFileSync(deltaPath, "utf8");
|
|
1728
|
+
const current = existsSync(profile.patchPath) ? readFileSync(profile.patchPath, "utf8") : "";
|
|
1729
|
+
let merged = current;
|
|
1242
1730
|
const written = [];
|
|
1243
1731
|
for (const base of selected) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const requiredService = ability?.requires?.service;
|
|
1732
|
+
if (typeof base.unsupported === "string") return err(`Base "${base.name}" is registered as UNSUPPORTED: ${base.unsupported}`);
|
|
1733
|
+
const requiredService = base.requires?.service;
|
|
1247
1734
|
if (typeof requiredService === "string" && ctx.get(requiredService) === void 0) return err(`Base "${base.name}" requires the "${requiredService}" service, which this deployment does not provide — the generated preset would refuse to mount.`);
|
|
1248
|
-
const
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
}, {
|
|
1257
|
-
name: "preset.yml",
|
|
1258
|
-
content: readFileSync(presetPath)
|
|
1259
|
-
}], void 0, (message) => {
|
|
1260
|
-
ctx.logger.warn(message);
|
|
1735
|
+
const basePatch = resolvePresetBasePatch(base.name, profile);
|
|
1736
|
+
const rowId = presetRowId(base.id);
|
|
1737
|
+
const entryText = composePresetEntry(readFileSync(basePatch, "utf8"), delta, {
|
|
1738
|
+
rowId,
|
|
1739
|
+
id: base.id,
|
|
1740
|
+
name: base.display.name,
|
|
1741
|
+
description: base.display.description,
|
|
1742
|
+
order: base.display.order
|
|
1261
1743
|
});
|
|
1262
|
-
|
|
1744
|
+
const next = mergePresetRow(merged, entryText, rowId);
|
|
1745
|
+
written.push(next === merged ? `${base.name} → ${profile.patchPath} (already current)` : `${base.name} → ${rowId} in ${profile.patchPath}`);
|
|
1746
|
+
merged = next;
|
|
1263
1747
|
}
|
|
1264
|
-
|
|
1748
|
+
if (merged !== current) atomicWriteFiles(dirname(profile.patchPath), [{
|
|
1749
|
+
name: PROFILE_PATCH_FILENAME$1,
|
|
1750
|
+
content: presetPatchText(merged)
|
|
1751
|
+
}], void 0, (message) => {
|
|
1752
|
+
ctx.logger.warn(message);
|
|
1753
|
+
});
|
|
1754
|
+
return ok(`Evolution agent preset row installed (${written.join("; ")}). Restart the session switcher to select it.`);
|
|
1265
1755
|
} catch (error) {
|
|
1266
1756
|
return err(`Preset install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1267
1757
|
}
|
|
@@ -1374,7 +1864,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1374
1864
|
a2: entry.tier,
|
|
1375
1865
|
a3: entry.owner
|
|
1376
1866
|
}));
|
|
1377
|
-
const namespace =
|
|
1867
|
+
const namespace = paramSettingsId(entry.owner);
|
|
1378
1868
|
if (namespace === void 0) return err(errorText("e-315-id-has-no-user", {
|
|
1379
1869
|
a1: id,
|
|
1380
1870
|
a2: entry.owner
|
|
@@ -1415,6 +1905,15 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1415
1905
|
wasOverridden: descriptor.user !== void 0 && Object.hasOwn(descriptor.user, id)
|
|
1416
1906
|
}));
|
|
1417
1907
|
}
|
|
1908
|
+
if (input === "migrate") {
|
|
1909
|
+
const attempt = await migrateFromContext(ctx);
|
|
1910
|
+
if (!attempt.ok) {
|
|
1911
|
+
if (attempt.problem === "settings") return err(errorText("e-311-no-settings-service-is"));
|
|
1912
|
+
if (attempt.problem === "home") return err(errorText("e-320-the-profile-home-is-not"));
|
|
1913
|
+
return err(errorText("e-321-no-io-provider-is"));
|
|
1914
|
+
}
|
|
1915
|
+
return ok(renderNamespaceMigration(attempt.outcome));
|
|
1916
|
+
}
|
|
1418
1917
|
if (input === "doctor" || input === "doctor --json") {
|
|
1419
1918
|
const report = await diagnose(ctx, { home: evolutionRoot() });
|
|
1420
1919
|
if (input === "doctor --json") return ok(JSON.stringify(report, null, 2));
|
|
@@ -1429,10 +1928,40 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1429
1928
|
};
|
|
1430
1929
|
commandCtx.effect(() => commands.register(evolutionCommand));
|
|
1431
1930
|
});
|
|
1931
|
+
let migrationRunning = false;
|
|
1932
|
+
const runMigrationOnce = () => {
|
|
1933
|
+
if (migrationRunning) return;
|
|
1934
|
+
migrationRunning = true;
|
|
1935
|
+
(async () => {
|
|
1936
|
+
try {
|
|
1937
|
+
const attempt = await migrateFromContext(ctx);
|
|
1938
|
+
if (!attempt.ok) {
|
|
1939
|
+
migrationRunning = false;
|
|
1940
|
+
ctx.logger.warn("evolution-commands: the legacy settings migration cannot run here (no %s) — run /evolution migrate after that service is mounted", attempt.problem);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
const { outcome } = attempt;
|
|
1944
|
+
if (outcome.source !== void 0 && outcome.report !== void 0 && outcome.report.written.length > 0) ctx.logger.info("evolution-commands: migrated the legacy settings document (%s)", renderNamespaceMigration(outcome));
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
migrationRunning = false;
|
|
1947
|
+
ctx.logger.warn("evolution-commands: the legacy settings migration did not run — %s", error instanceof Error ? error.message : String(error));
|
|
1948
|
+
}
|
|
1949
|
+
})();
|
|
1950
|
+
};
|
|
1951
|
+
ctx.inject(["settings"], () => {
|
|
1952
|
+
runMigrationOnce();
|
|
1953
|
+
});
|
|
1954
|
+
(async () => {
|
|
1955
|
+
const loader = ctx.root.loader;
|
|
1956
|
+
try {
|
|
1957
|
+
await loader?.await?.();
|
|
1958
|
+
} catch {}
|
|
1959
|
+
runMigrationOnce();
|
|
1960
|
+
})();
|
|
1432
1961
|
}
|
|
1433
1962
|
/**
|
|
1434
1963
|
* 0.3.14 (P1-1): locate the installed `dsh-evolution-agent-preset` package
|
|
1435
|
-
* (the delivery container for agent.cordis.yml/
|
|
1964
|
+
* (the delivery container for agent.cordis.yml/bases.json). Resolution order:
|
|
1436
1965
|
* npm-name sibling (published profile layout), dev-tree sibling (source/test
|
|
1437
1966
|
* layout), then module resolution. The package dir name differs from the npm
|
|
1438
1967
|
* name (`evolution-agent` vs `@lmzhen/dsh-evolution-agent-preset` — the rc.44
|
|
@@ -1449,11 +1978,22 @@ function readAgentPresetBases(source) {
|
|
|
1449
1978
|
if (!Array.isArray(parsed.bases) || parsed.bases.length === 0) throw new Error(`${path} carries no bases[]`);
|
|
1450
1979
|
const entries = parsed.bases.map((raw) => {
|
|
1451
1980
|
const entry = raw;
|
|
1452
|
-
if (typeof entry.name !== "string" || typeof entry.id !== "string"
|
|
1981
|
+
if (typeof entry.name !== "string" || typeof entry.id !== "string") throw new Error(`${path} entry ${JSON.stringify(raw)} needs name/id strings`);
|
|
1982
|
+
const display = entry.display;
|
|
1983
|
+
if (display === null || typeof display !== "object" || typeof display.name !== "string" || display.name === "" || typeof display.description !== "string" || display.description === "" || typeof display.order !== "number" || !Number.isInteger(display.order)) throw new Error(`${path} entry "${entry.name}" needs display { name, description, order }`);
|
|
1984
|
+
const requires = entry.requires;
|
|
1985
|
+
if (requires !== void 0 && (requires === null || typeof requires !== "object" || typeof requires.service !== "string")) throw new Error(`${path} entry "${entry.name}" needs requires.service as a string`);
|
|
1986
|
+
if (entry.unsupported !== void 0 && (typeof entry.unsupported !== "string" || entry.unsupported === "")) throw new Error(`${path} entry "${entry.name}" needs unsupported as a non-empty reason string`);
|
|
1453
1987
|
return {
|
|
1454
1988
|
name: entry.name,
|
|
1455
1989
|
id: entry.id,
|
|
1456
|
-
|
|
1990
|
+
display: {
|
|
1991
|
+
name: display.name,
|
|
1992
|
+
description: display.description,
|
|
1993
|
+
order: display.order
|
|
1994
|
+
},
|
|
1995
|
+
...requires === void 0 ? {} : { requires: { service: requires.service } },
|
|
1996
|
+
...entry.unsupported === void 0 ? {} : { unsupported: entry.unsupported }
|
|
1457
1997
|
};
|
|
1458
1998
|
});
|
|
1459
1999
|
const declared = parsed.default;
|
|
@@ -1465,7 +2005,7 @@ function readAgentPresetBases(source) {
|
|
|
1465
2005
|
}
|
|
1466
2006
|
function resolveAgentPresetDir(importMetaUrl) {
|
|
1467
2007
|
const overlayCandidate = join(dirname(fileURLToPath(importMetaUrl)), "..", "..", "evolution-agent");
|
|
1468
|
-
if (existsSync(join(overlayCandidate, "agent.cordis.yml")) && existsSync(join(overlayCandidate, "
|
|
2008
|
+
if (existsSync(join(overlayCandidate, "agent.cordis.yml")) && existsSync(join(overlayCandidate, "bases.json"))) return overlayCandidate;
|
|
1469
2009
|
try {
|
|
1470
2010
|
return dirname(createRequire(importMetaUrl).resolve("@lmzhen/dsh-evolution-agent-preset/package.json"));
|
|
1471
2011
|
} catch {
|