@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 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
  };
@@ -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.77",
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.77",
31
- "@lmzhen/dsh-evolution-core": "^0.3.77",
32
- "@lmzhen/dsh-evolution-maintenance": "^0.3.77"
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.77"
35
+ "@lmzhen/dsh-evolution-agent-preset": "^0.3.79"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",