@lmzhen/dsh-evolution-commands 0.3.77 → 0.3.79
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 +6 -0
- package/lib/index.js +178 -2
- package/lib/types/doctor.d.ts +18 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -29,5 +29,11 @@ Independent of request-prefix construction. This package does not alter the asse
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
- No known durable consumer gaps at this time. Runtime contracts are covered by package and boundary tests.
|
|
32
|
+
- **① variant form: the `/evolution …` command face is visible in every session**
|
|
33
|
+
(accepted, not fixed — 0.3.78). Commands are registered in scope layers, and the
|
|
34
|
+
family registers once on the host plane, so a session on a platform original
|
|
35
|
+
preset still lists the management-only subcommands even though no automatic
|
|
36
|
+
family behaviour reaches it. The difference and its reasoning are single-sourced
|
|
37
|
+
in `INSTALL.md` ("Known difference").
|
|
32
38
|
|
|
33
39
|
**Runtime invariant:** No companion is published. The platform auto-assembles nothing and the family mounts no `<pkg>/invariant` cordis row, so a companion here would never execute (v37 S2.1 / I-3).
|
package/lib/index.js
CHANGED
|
@@ -102,6 +102,165 @@ function memoryInterpolationIssues(home) {
|
|
|
102
102
|
}
|
|
103
103
|
return issues;
|
|
104
104
|
}
|
|
105
|
+
/** G3-② (B2): the family delta container. An optionalDependency of this
|
|
106
|
+
* package, so resolution may legitimately fail; every failure degrades to
|
|
107
|
+
* "unknown" — the doctor never throws.
|
|
108
|
+
* Platform anchor: agentPresets.read(id) — preset/agent-presets/src/index.ts:501. */
|
|
109
|
+
const AGENT_PRESET_PACKAGE = "@lmzhen/dsh-evolution-agent-preset";
|
|
110
|
+
/** Resolve one asset the package declares in its exports map.
|
|
111
|
+
* @param asset - bases.json or agent.cordis.yml.
|
|
112
|
+
* @returns the file path, or null when the package is not installed. */
|
|
113
|
+
function resolveAgentPresetAsset(asset) {
|
|
114
|
+
try {
|
|
115
|
+
return fileURLToPath(import.meta.resolve(`${AGENT_PRESET_PACKAGE}/${asset}`));
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** The base rows of bases.json: the comparison iterates the same table the
|
|
121
|
+
* installer reads, so doctor cannot check a different set of variants.
|
|
122
|
+
* @param path - resolved bases.json.
|
|
123
|
+
* @returns the rows, or null when the table is unreadable or malformed. */
|
|
124
|
+
function readAgentPresetBases$1(path) {
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
127
|
+
if (!Array.isArray(parsed.bases)) return null;
|
|
128
|
+
const rows = [];
|
|
129
|
+
for (const raw of parsed.bases) {
|
|
130
|
+
const entry = raw;
|
|
131
|
+
if (typeof entry.name !== "string" || typeof entry.id !== "string") return null;
|
|
132
|
+
rows.push({
|
|
133
|
+
name: entry.name,
|
|
134
|
+
id: entry.id
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return rows.length > 0 ? rows : null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Preset directories already on disk, used to keep an installed variant
|
|
143
|
+
* VISIBLE when the base table itself could not be read.
|
|
144
|
+
* @param root - the .agent-presets directory.
|
|
145
|
+
* @returns the directory names; empty when nothing is installed. */
|
|
146
|
+
function installedPresetIds(root) {
|
|
147
|
+
const ids = [];
|
|
148
|
+
let entries;
|
|
149
|
+
try {
|
|
150
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
151
|
+
} catch {
|
|
152
|
+
return ids;
|
|
153
|
+
}
|
|
154
|
+
for (const entry of entries) if (entry.isDirectory()) ids.push(entry.name);
|
|
155
|
+
return ids;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* G3-② (B2): recompute what a fresh generation would write for every
|
|
159
|
+
* installed variant and compare it byte-for-byte with the file on disk.
|
|
160
|
+
*
|
|
161
|
+
* The recomputation goes through composePresetComposition — the SAME rule
|
|
162
|
+
* /evolution preset install and install-layered.mjs apply (collision guard +
|
|
163
|
+
* row-overrides.json), so doctor cannot disagree with the installer about what
|
|
164
|
+
* "fresh" means. Read-only by contract: the snapshot is never rewritten.
|
|
165
|
+
* @param home - resolved DSH_HOME.
|
|
166
|
+
* @param registry - the platform agent-preset registry, undefined when unmounted.
|
|
167
|
+
* @returns one row per base in the family bases.json.
|
|
168
|
+
*/
|
|
169
|
+
async function probePresetFreshness(home, registry) {
|
|
170
|
+
const presetsRoot = join(home, ".agent-presets");
|
|
171
|
+
const basesPath = resolveAgentPresetAsset("bases.json");
|
|
172
|
+
const deltaPath = resolveAgentPresetAsset("agent.cordis.yml");
|
|
173
|
+
const bases = basesPath === null ? null : readAgentPresetBases$1(basesPath);
|
|
174
|
+
let delta = null;
|
|
175
|
+
let deltaFailure = `${AGENT_PRESET_PACKAGE}/agent.cordis.yml could not be resolved — is the family agent-preset package installed?`;
|
|
176
|
+
if (deltaPath !== null) try {
|
|
177
|
+
delta = readFileSync(deltaPath, "utf8");
|
|
178
|
+
} catch (error) {
|
|
179
|
+
deltaFailure = `${deltaPath} could not be read (${error instanceof Error ? error.message : String(error)})`;
|
|
180
|
+
}
|
|
181
|
+
if (bases === null) {
|
|
182
|
+
const tablePath = basesPath ?? `${AGENT_PRESET_PACKAGE}/bases.json`;
|
|
183
|
+
return installedPresetIds(presetsRoot).map((id) => ({
|
|
184
|
+
base: id,
|
|
185
|
+
destination: join(presetsRoot, id),
|
|
186
|
+
status: "unknown",
|
|
187
|
+
detail: `the family base table could not be read (${tablePath}) — a fresh generation cannot be recomputed`
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
const rows = [];
|
|
191
|
+
for (const base of bases) {
|
|
192
|
+
const destination = join(presetsRoot, base.id);
|
|
193
|
+
const compositionPath = join(destination, "agent.cordis.yml");
|
|
194
|
+
if (!existsSync(compositionPath)) {
|
|
195
|
+
rows.push({
|
|
196
|
+
base: base.name,
|
|
197
|
+
destination,
|
|
198
|
+
status: "absent"
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (registry === void 0) {
|
|
203
|
+
rows.push({
|
|
204
|
+
base: base.name,
|
|
205
|
+
destination,
|
|
206
|
+
status: "unknown",
|
|
207
|
+
detail: "the platform agent-preset registry is not mounted, so the runtime composition cannot be read"
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (delta === null) {
|
|
212
|
+
rows.push({
|
|
213
|
+
base: base.name,
|
|
214
|
+
destination,
|
|
215
|
+
status: "unknown",
|
|
216
|
+
detail: deltaFailure
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
let platform;
|
|
221
|
+
try {
|
|
222
|
+
platform = await registry.read(base.name);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
rows.push({
|
|
225
|
+
base: base.name,
|
|
226
|
+
destination,
|
|
227
|
+
status: "unknown",
|
|
228
|
+
detail: `the runtime composition for base "${base.name}" could not be read (${error instanceof Error ? error.message : String(error)})`
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
let fresh;
|
|
233
|
+
try {
|
|
234
|
+
fresh = composePresetComposition(platform, delta);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
rows.push({
|
|
237
|
+
base: base.name,
|
|
238
|
+
destination,
|
|
239
|
+
status: "unknown",
|
|
240
|
+
detail: `a fresh generation refused to compose (${error instanceof Error ? error.message : String(error)})`
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
let onDisk;
|
|
245
|
+
try {
|
|
246
|
+
onDisk = readFileSync(compositionPath, "utf8");
|
|
247
|
+
} catch (error) {
|
|
248
|
+
rows.push({
|
|
249
|
+
base: base.name,
|
|
250
|
+
destination,
|
|
251
|
+
status: "unknown",
|
|
252
|
+
detail: `${compositionPath} could not be read (${error instanceof Error ? error.message : String(error)})`
|
|
253
|
+
});
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
rows.push({
|
|
257
|
+
base: base.name,
|
|
258
|
+
destination,
|
|
259
|
+
status: fresh === onDisk ? "fresh" : "differs"
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return rows;
|
|
263
|
+
}
|
|
105
264
|
/**
|
|
106
265
|
* Diagnose the deployment. `ctx` supplies service presence (a bare stub with
|
|
107
266
|
* only `get` is enough); `home` defaults to the evolution root so tests can
|
|
@@ -115,6 +274,7 @@ async function diagnose(ctx, options = {}) {
|
|
|
115
274
|
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`);
|
|
116
275
|
});
|
|
117
276
|
const has = (name) => ctx.get(name) !== void 0;
|
|
277
|
+
const presetFreshness = await probePresetFreshness(home, ctx.get("agentPresets"));
|
|
118
278
|
const full = bundles.some((name) => tailOf(name) === "dsh-evolution-all");
|
|
119
279
|
const host = bundles.some((name) => tailOf(name) === "dsh-evolution-host");
|
|
120
280
|
const preset = bundles.some((name) => tailOf(name) === "dsh-evolution-preset");
|
|
@@ -161,6 +321,10 @@ async function diagnose(ctx, options = {}) {
|
|
|
161
321
|
const memoryIssues = memoryInterpolationIssues(home);
|
|
162
322
|
if (memoryIssues.length > 0) actions.push("Rewrite the memory entries listed above (or run a build with the render-time neutralization) — they broke prompt assembly on older builds.");
|
|
163
323
|
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.`);
|
|
324
|
+
for (const row of presetFreshness) {
|
|
325
|
+
if (row.status !== "differs") continue;
|
|
326
|
+
actions.push(`Preset variant ${row.destination} DIFFERS from a fresh generation — it is an install-time snapshot; re-run the installer (/evolution preset install) to regenerate`);
|
|
327
|
+
}
|
|
164
328
|
return {
|
|
165
329
|
installForm,
|
|
166
330
|
deploymentForm,
|
|
@@ -171,6 +335,7 @@ async function diagnose(ctx, options = {}) {
|
|
|
171
335
|
services,
|
|
172
336
|
pendingCount,
|
|
173
337
|
executingCount,
|
|
338
|
+
presetFreshness,
|
|
174
339
|
actions
|
|
175
340
|
};
|
|
176
341
|
}
|
|
@@ -182,6 +347,11 @@ function renderDoctorText(report) {
|
|
|
182
347
|
`pending: ${report.pendingCount === null ? "unknown" : report.pendingCount}`,
|
|
183
348
|
`executing: ${report.executingCount === null ? "unknown" : report.executingCount}`
|
|
184
349
|
];
|
|
350
|
+
for (const row of report.presetFreshness) {
|
|
351
|
+
if (row.status === "absent") continue;
|
|
352
|
+
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"})`;
|
|
353
|
+
lines.push(`preset: ${row.destination} ${verdict}`);
|
|
354
|
+
}
|
|
185
355
|
if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
|
|
186
356
|
if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
|
|
187
357
|
if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
|
|
@@ -583,12 +753,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
583
753
|
if (entry === void 0) return err(`Unknown agent-preset base "${name}" — ${join(source, "bases.json")} lists ${table.entries.map((candidate) => candidate.name).join(", ")}`);
|
|
584
754
|
selected.push(entry);
|
|
585
755
|
}
|
|
756
|
+
const rawBases = JSON.parse(readFileSync(join(source, "bases.json"), "utf8"));
|
|
586
757
|
const deltaPath = join(source, "agent.cordis.yml");
|
|
587
758
|
if (!existsSync(deltaPath)) return err(`Preset file missing from ${source} — is the dsh-evolution-agent-preset package installed?`);
|
|
588
759
|
const registry = ctx.get("agentPresets");
|
|
589
760
|
if (!registry) return err("Agent preset registry not mounted — cannot compose the Evolution preset against the runtime standard.");
|
|
590
761
|
const written = [];
|
|
591
762
|
for (const base of selected) {
|
|
763
|
+
const ability = rawBases.bases?.find((candidate) => candidate.name === base.name);
|
|
764
|
+
if (typeof ability?.unsupported === "string") return err(`Base "${base.name}" is registered as UNSUPPORTED: ${ability.unsupported}`);
|
|
765
|
+
const requiredService = ability?.requires?.service;
|
|
766
|
+
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.`);
|
|
592
767
|
const target = join(evolutionRoot(), ".agent-presets", base.id);
|
|
593
768
|
const presetPath = join(source, base.metadata);
|
|
594
769
|
if (!existsSync(presetPath)) return err(`Preset file missing from ${source} — is the dsh-evolution-agent-preset package installed?`);
|
|
@@ -740,8 +915,9 @@ const defaultFs = {
|
|
|
740
915
|
mtime: (path) => {
|
|
741
916
|
try {
|
|
742
917
|
return statSync(path).mtimeMs;
|
|
743
|
-
} catch {
|
|
744
|
-
return null;
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
920
|
+
throw error;
|
|
745
921
|
}
|
|
746
922
|
}
|
|
747
923
|
};
|
package/lib/types/doctor.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/** G3-② (B2): one delivered preset variant compared against a fresh generation.
|
|
2
|
+
* "absent" is the user simply not using that base (never an action); "unknown"
|
|
3
|
+
* is a comparison that could not run, kept distinct from a verified "fresh". */
|
|
4
|
+
export interface PresetFreshnessRow {
|
|
5
|
+
/** Agent-preset base name (the id the platform registry read() takes). */
|
|
6
|
+
base: string;
|
|
7
|
+
/** Install destination <home>/.agent-presets/<base.id> that was compared. */
|
|
8
|
+
destination: string;
|
|
9
|
+
status: 'fresh' | 'differs' | 'absent' | 'unknown';
|
|
10
|
+
/** Why the comparison could not run — set on unknown rows only. */
|
|
11
|
+
detail?: string;
|
|
12
|
+
}
|
|
1
13
|
export interface DoctorReport {
|
|
2
14
|
/** OPT-23 (2026-09): `preset-only` — the delivered Evolution preset
|
|
3
15
|
* artifact exists but NO profile carries an evolution bundle (e.g. the
|
|
@@ -38,6 +50,12 @@ export interface DoctorReport {
|
|
|
38
50
|
/** v23 (AP-2): claimed-but-crashed records — the only state that needs an
|
|
39
51
|
* operator action (reject) to clear; surfaced separately from pending. */
|
|
40
52
|
executingCount: number | null;
|
|
53
|
+
/** G3-② (B2): how the delivered preset variants compare with what a fresh
|
|
54
|
+
* generation would write against THIS runtime platform. A variant embeds the
|
|
55
|
+
* platform composition of its install day, so a platform change or a family
|
|
56
|
+
* upgrade leaves a file describing a platform that no longer exists. Read-only:
|
|
57
|
+
* a stale snapshot is reported, never repaired. */
|
|
58
|
+
presetFreshness: PresetFreshnessRow[];
|
|
41
59
|
actions: string[];
|
|
42
60
|
}
|
|
43
61
|
/** Profile bundle rows for evolution-family packages across all profiles.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-commands",
|
|
3
3
|
"description": "Human commands for the evolution family (/evolution pending|curator|maintain|doctor) (community build)",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.79",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.3.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
32
|
-
"@lmzhen/dsh-evolution-maintenance": "^0.3.
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.79",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.3.79",
|
|
32
|
+
"@lmzhen/dsh-evolution-maintenance": "^0.3.79"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@lmzhen/dsh-evolution-agent-preset": "^0.3.
|
|
35
|
+
"@lmzhen/dsh-evolution-agent-preset": "^0.3.79"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@deepseek-ai/cordis": "^4.0.1",
|