dsh-crate-web 0.1.0 → 0.1.2-preview.2

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -290
  3. package/README.zh-CN.md +2 -303
  4. package/THIRD-PARTY-LICENSE.txt +21 -0
  5. package/cordis.patch.yml +1 -6
  6. package/core/dsh_pack/_version.py +1 -0
  7. package/core/dsh_pack/bundle.py +74 -0
  8. package/core/dsh_pack/cli.py +81 -18
  9. package/core/dsh_pack/desktop_verify.py +304 -0
  10. package/core/dsh_pack/diagnostics.py +1170 -0
  11. package/core/dsh_pack/errors.py +16 -1
  12. package/core/dsh_pack/export.py +86 -103
  13. package/core/dsh_pack/export_plan.py +96 -0
  14. package/core/dsh_pack/identity.py +25 -0
  15. package/core/dsh_pack/importer.py +1598 -1347
  16. package/core/dsh_pack/network.py +5 -4
  17. package/core/dsh_pack/pack.py +14 -2
  18. package/core/dsh_pack/portability.py +392 -0
  19. package/core/dsh_pack/preflight.py +63 -16
  20. package/core/dsh_pack/troubleshooting.py +94 -0
  21. package/core/dsh_pack/user_cli.py +16 -0
  22. package/core/dsh_pack/validation.py +64 -4
  23. package/core/dsh_pack/verify.py +1485 -1336
  24. package/core/dsh_pack/windows_verify.py +177 -0
  25. package/desktop-patch-helper.mjs +140 -0
  26. package/lib/client.js +292 -102
  27. package/lib/index.d.ts +7 -1
  28. package/lib/index.js +1121 -229
  29. package/package.json +13 -5
  30. package/restart-helper.mjs +121 -121
  31. package/runtime/python/LICENSE.txt +676 -676
  32. package/runtime/python/__install__.json +81 -81
  33. package/runtime/python/python314._pth +6 -6
  34. package/skills/dsh-crate-troubleshooting/SKILL.md +172 -0
  35. package/skills/dsh-crate-troubleshooting/compatibility.json +11 -0
  36. package/skills/dsh-crate-troubleshooting/reference/diagnostics.generated.md +96 -0
  37. package/skills/dsh-crate-troubleshooting/reference/repair-boundaries.md +60 -0
  38. package/skills/dsh-crate-troubleshooting/reference/stages.generated.md +22 -0
  39. package/verify-desktop.mjs +314 -0
@@ -0,0 +1,1170 @@
1
+ """Version-aware diagnostic protocol and Diagnostic Registry for DSH Crate.
2
+
3
+ Design (P7): the registry in this module is the single source of truth for the
4
+ diagnostic protocol:
5
+
6
+ - ``DIAGNOSTIC_SCHEMA_VERSION`` — generation of the diagnostic envelope.
7
+ - ``CRATE_VERSION`` — the DSH Crate version that produced the diagnostic.
8
+ - ``DIAGNOSTIC_SPECS`` — every known diagnostic code.
9
+ - ``STAGE_SPECS`` — every known failure stage.
10
+ - repair permission levels (L0..L3) and write scopes.
11
+
12
+ Reference documents under ``skills/dsh-crate-troubleshooting/reference/`` are
13
+ generated from this module by ``scripts/generate-skill-reference.py``; never
14
+ hand-edit generated files. The release workflow fails when the generated
15
+ reference is out of date (anti-drift).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from typing import Any, Mapping
22
+
23
+ from ._version import __version__ as CRATE_VERSION
24
+
25
+ DIAGNOSTIC_SCHEMA_VERSION = 1
26
+ PRODUCER = "dsh-crate"
27
+
28
+ # Repair permission levels (see skills/dsh-crate-troubleshooting/SKILL.md).
29
+ REPAIR_LEVEL_READ_ONLY = "L0"
30
+ REPAIR_LEVEL_SAFE = "L1"
31
+ REPAIR_LEVEL_CONFIRMATION = "L2"
32
+ REPAIR_LEVEL_FORBIDDEN = "L3"
33
+
34
+ # Write scopes used by repair recipes.
35
+ WRITE_SCOPE_NONE = "none"
36
+ WRITE_SCOPE_TEMPORARY_PROFILE = "temporary-profile"
37
+ WRITE_SCOPE_TARGET_PROFILE = "target-profile"
38
+ WRITE_SCOPE_ENVIRONMENT = "environment"
39
+ WRITE_SCOPE_DSH_HOME = "dsh-home"
40
+
41
+ SEVERITY_BLOCKER = "BLOCKER"
42
+ SEVERITY_WARNING = "WARNING"
43
+ SEVERITY_INFO = "INFO"
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Stages
47
+ # ---------------------------------------------------------------------------
48
+
49
+ STAGE_SPECS: dict[str, dict[str, Any]] = {
50
+ "planning": {
51
+ "title": "Plan",
52
+ "description": "Operation planning: target Profile selection, name validation, pack read, and Import preview.",
53
+ "importState": "planning",
54
+ },
55
+ "preflight": {
56
+ "title": "Preflight",
57
+ "description": "Read-only Inspect/Preflight of a Pack before any Profile change.",
58
+ "importState": "preflight",
59
+ },
60
+ "configuration": {
61
+ "title": "Configuration",
62
+ "description": "Writing Pack profile configuration into the target Profile.",
63
+ "importState": "configuration",
64
+ },
65
+ "embedded-install": {
66
+ "title": "Embedded install",
67
+ "description": "Installing an embedded plugin artifact (npm tarball) into the temporary Profile.",
68
+ "importState": "embedded-install",
69
+ },
70
+ "reference-install": {
71
+ "title": "Reference install",
72
+ "description": "Installing or resolving a reference-only plugin source.",
73
+ "importState": "reference-install",
74
+ },
75
+ "network-install": {
76
+ "title": "Network install",
77
+ "description": "Downloading and installing a recorded network package.",
78
+ "importState": "network-install",
79
+ },
80
+ "version-confirmation": {
81
+ "title": "Version confirmation",
82
+ "description": "Confirming the installed package identity matches the Pack resolution.",
83
+ "importState": "version-confirmation",
84
+ },
85
+ "composition": {
86
+ "title": "Bundle composition",
87
+ "description": "Composing Bundle patches and Loader rows for the Profile.",
88
+ "importState": "composition",
89
+ },
90
+ "commit": {
91
+ "title": "Commit",
92
+ "description": "Committing the prepared Profile and metadata as a successful Import.",
93
+ "importState": "commit",
94
+ },
95
+ "delete": {
96
+ "title": "Delete",
97
+ "description": "Deleting a non-running Profile after explicit confirmation.",
98
+ "importState": "delete",
99
+ },
100
+ "restart": {
101
+ "title": "Restart",
102
+ "description": "Restarting DSH on the target Profile and port.",
103
+ "importState": "restart",
104
+ },
105
+ "runtime": {
106
+ "title": "Runtime boot",
107
+ "description": "Starting the DSH runtime and waiting for a ready URL.",
108
+ "importState": "runtime",
109
+ },
110
+ "probe": {
111
+ "title": "Probe",
112
+ "description": "Running a runtime capability probe after boot.",
113
+ "importState": "probe",
114
+ },
115
+ "plugin-smoke": {
116
+ "title": "Plugin smoke",
117
+ "description": "Running a plugin-specific smoke test against the runtime.",
118
+ "importState": "plugin-smoke",
119
+ },
120
+ "environment": {
121
+ "title": "Environment",
122
+ "description": "Environment compatibility findings (OS, Node, DSH versions).",
123
+ "importState": "environment",
124
+ },
125
+ "surface": {
126
+ "title": "Surface",
127
+ "description": "Web surface or Bundle composition surface checks.",
128
+ "importState": "surface",
129
+ },
130
+ }
131
+
132
+
133
+ def _spec(
134
+ *,
135
+ stage: str,
136
+ severity: str,
137
+ summary: str,
138
+ description: str,
139
+ expected: str,
140
+ impact: str,
141
+ can_continue: bool,
142
+ suggested_checks: list[str],
143
+ repair_level: str,
144
+ write_scope: str,
145
+ verify_after: bool = True,
146
+ ) -> dict[str, Any]:
147
+ return {
148
+ "stage": stage,
149
+ "severity": severity,
150
+ "summary": summary,
151
+ "description": description,
152
+ "expected": expected,
153
+ "impact": impact,
154
+ "canContinue": can_continue,
155
+ "suggestedChecks": suggested_checks,
156
+ "repairLevel": repair_level,
157
+ "writeScope": write_scope,
158
+ "verifyAfter": verify_after,
159
+ }
160
+
161
+
162
+ def _checks(*items: str) -> list[str]:
163
+ return list(items)
164
+ # ---------------------------------------------------------------------------
165
+ # Diagnostic code registry (single source of truth)
166
+ # ---------------------------------------------------------------------------
167
+
168
+ DIAGNOSTIC_SPECS: dict[str, dict[str, Any]] = {
169
+ # --- Planning (web API) ---
170
+ "PROFILE_NAME_RESERVED": _spec(
171
+ stage="planning", severity=SEVERITY_BLOCKER,
172
+ summary="Profile name is reserved.",
173
+ description="The requested Profile name is reserved by DSH and cannot be used.",
174
+ expected="the requested Profile name is available",
175
+ impact="The requested Profile was not created.",
176
+ can_continue=False,
177
+ suggested_checks=_checks("choose another Profile name", "verify DSH reserved-name rules"),
178
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
179
+ ),
180
+ "PROFILE_EXISTS": _spec(
181
+ stage="planning", severity=SEVERITY_BLOCKER,
182
+ summary="Target Profile already exists.",
183
+ description="A Profile with the requested name already exists in this DSH_HOME.",
184
+ expected="the target Profile name is new or the overwrite is confirmed",
185
+ impact="No Profile was created or modified.",
186
+ can_continue=False,
187
+ suggested_checks=_checks("choose a different Profile name", "delete the existing Profile first", "use explicit overwrite mode with confirmation"),
188
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_TARGET_PROFILE,
189
+ ),
190
+ "PROFILE_MISSING": _spec(
191
+ stage="planning", severity=SEVERITY_BLOCKER,
192
+ summary="Target Profile does not exist.",
193
+ description="The selected Profile does not exist in the current DSH_HOME.",
194
+ expected="the selected Profile exists",
195
+ impact="The operation was not applied.",
196
+ can_continue=False,
197
+ suggested_checks=_checks("verify the Profile name", "choose an existing Profile"),
198
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
199
+ ),
200
+ "OFFICIAL_BUNDLE_MISSING": _spec(
201
+ stage="planning", severity=SEVERITY_BLOCKER,
202
+ summary="Official DSH Bundle cannot be resolved.",
203
+ description="An official DSH Bundle version could not be resolved for this Profile.",
204
+ expected="the official DSH Bundles are installed in this DSH_HOME",
205
+ impact="The requested Profile was not created.",
206
+ can_continue=False,
207
+ suggested_checks=_checks("install the official DSH bundles into this DSH_HOME", "verify the Bundle list", "retry after Bundle installation"),
208
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_DSH_HOME,
209
+ ),
210
+ "CRATE_VERSION_MISSING": _spec(
211
+ stage="planning", severity=SEVERITY_BLOCKER,
212
+ summary="DSH Crate package version is missing.",
213
+ description="The installed DSH Crate package does not expose its version, so the diagnostic protocol cannot stamp it.",
214
+ expected="the DSH Crate installation exposes its package version",
215
+ impact="The requested Profile was not created.",
216
+ can_continue=False,
217
+ suggested_checks=_checks("repair the DSH Crate installation", "reinstall dsh-crate-web and retry"),
218
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_DSH_HOME,
219
+ ),
220
+ "ACTIVE_PROFILE": _spec(
221
+ stage="delete", severity=SEVERITY_BLOCKER,
222
+ summary="Cannot delete the active Profile.",
223
+ description="The running DSH process uses this Profile; deleting it would break the running environment.",
224
+ expected="the deleted Profile is not the active runtime Profile",
225
+ impact="The running DSH process would lose its Profile files; deletion was refused.",
226
+ can_continue=False,
227
+ suggested_checks=_checks("switch to another Profile first", "then delete this Profile"),
228
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_TARGET_PROFILE,
229
+ ),
230
+ "SWITCH_CONFIRMATION_REQUIRED": _spec(
231
+ stage="planning", severity=SEVERITY_BLOCKER,
232
+ summary="Profile switch requires explicit confirmation.",
233
+ description="Switching the running Profile is a state-changing operation that requires the user to confirm.",
234
+ expected="the user confirms the switch",
235
+ impact="The current DSH process was not changed.",
236
+ can_continue=False,
237
+ suggested_checks=_checks("confirm the switch", "retry after confirmation"),
238
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_ENVIRONMENT,
239
+ ),
240
+ "RESTART_NOT_CONFIGURED": _spec(
241
+ stage="restart", severity=SEVERITY_BLOCKER,
242
+ summary="DSH restart launcher is not configured.",
243
+ description="The current DSH launch command cannot be safely reconstructed, so restart is refused.",
244
+ expected="the current DSH launch command can be reconstructed",
245
+ impact="The current DSH process was not changed.",
246
+ can_continue=False,
247
+ suggested_checks=_checks("start DSH with an explicit --profile and --port", "configure the DSH Crate restart launcher"),
248
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_ENVIRONMENT,
249
+ ),
250
+
251
+ # --- Import planning / prepare ---
252
+ "PROFILE_NAME_MISSING": _spec(
253
+ stage="planning", severity=SEVERITY_BLOCKER,
254
+ summary="Profile name is missing.",
255
+ description="The Pack or request did not provide a target Profile name.",
256
+ expected="a target Profile name is provided",
257
+ impact="Import did not start and no Profile was changed.",
258
+ can_continue=False,
259
+ suggested_checks=_checks("provide a target Profile name", "review the Import preview"),
260
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
261
+ ),
262
+ "PROFILE_NAME_INVALID": _spec(
263
+ stage="planning", severity=SEVERITY_BLOCKER,
264
+ summary="Profile name is invalid.",
265
+ description="The target Profile name does not satisfy DSH naming rules.",
266
+ expected="the Profile name is valid",
267
+ impact="Import did not start and no Profile was changed.",
268
+ can_continue=False,
269
+ suggested_checks=_checks("use a valid Profile name", "review the Import preview"),
270
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
271
+ ),
272
+ "PROFILE_NAME_UNAVAILABLE": _spec(
273
+ stage="planning", severity=SEVERITY_BLOCKER,
274
+ summary="Profile name is unavailable.",
275
+ description="The requested Profile name cannot be used in this DSH_HOME.",
276
+ expected="the requested Profile name is available",
277
+ impact="Import did not start and no Profile was changed.",
278
+ can_continue=False,
279
+ suggested_checks=_checks("choose a different Profile name", "review the Import preview"),
280
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
281
+ ),
282
+ "PROFILE_TARGET_INVALID": _spec(
283
+ stage="planning", severity=SEVERITY_BLOCKER,
284
+ summary="Import target is invalid.",
285
+ description="The import target Profile configuration is invalid.",
286
+ expected="the target Profile is valid",
287
+ impact="Import did not start and no Profile was changed.",
288
+ can_continue=False,
289
+ suggested_checks=_checks("review the target Profile settings", "correct the target and retry"),
290
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
291
+ ),
292
+ "PROFILE_TARGET_MISSING": _spec(
293
+ stage="planning", severity=SEVERITY_BLOCKER,
294
+ summary="Import target is missing.",
295
+ description="No target Profile was selected for the Import.",
296
+ expected="a target Profile is selected",
297
+ impact="Import did not start and no Profile was changed.",
298
+ can_continue=False,
299
+ suggested_checks=_checks("select a target Profile", "retry"),
300
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
301
+ ),
302
+ "PACKAGE_NAME_INVALID": _spec(
303
+ stage="planning", severity=SEVERITY_BLOCKER,
304
+ summary="Plugin package name is invalid.",
305
+ description="A plugin package name recorded in the Pack is not a valid npm package name.",
306
+ expected="every plugin package name is valid",
307
+ impact="Import did not start and no Profile was changed.",
308
+ can_continue=False,
309
+ suggested_checks=_checks("verify the Pack plugin identity", "re-export the Pack with a valid name"),
310
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
311
+ ),
312
+ "PACKAGE_MANIFEST_INVALID": _spec(
313
+ stage="planning", severity=SEVERITY_BLOCKER,
314
+ summary="Plugin package manifest is invalid.",
315
+ description="A plugin package.json inside the Pack is unreadable or malformed.",
316
+ expected="every plugin package.json is valid",
317
+ impact="Import did not start and no Profile was changed.",
318
+ can_continue=False,
319
+ suggested_checks=_checks("verify the Pack manifest", "re-export the Pack"),
320
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
321
+ ),
322
+ "PACK_READ_ERROR": _spec(
323
+ stage="planning", severity=SEVERITY_BLOCKER,
324
+ summary="Pack cannot be read.",
325
+ description="The .dshcrate file could not be read or parsed.",
326
+ expected="the Pack is readable",
327
+ impact="Import did not start and no Profile was changed.",
328
+ can_continue=False,
329
+ suggested_checks=_checks("verify the Pack file is not corrupt", "re-download or re-export the Pack"),
330
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
331
+ ),
332
+ "PREFLIGHT_ERROR": _spec(
333
+ stage="preflight", severity=SEVERITY_BLOCKER,
334
+ summary="Preflight crashed.",
335
+ description="Preflight raised an unexpected error while inspecting the Pack.",
336
+ expected="Preflight completes",
337
+ impact="Inspect/Preflight could not finish.",
338
+ can_continue=False,
339
+ suggested_checks=_checks("inspect the Preflight error evidence", "retry Inspect"),
340
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
341
+ ),
342
+ "PREFLIGHT_BLOCKED": _spec(
343
+ stage="preflight", severity=SEVERITY_BLOCKER,
344
+ summary="Preflight found BLOCKER findings.",
345
+ description="The Pack cannot be imported because Preflight reported blocking findings.",
346
+ expected="no BLOCKER findings",
347
+ impact="Import is not allowed until the BLOCKER findings are resolved.",
348
+ can_continue=False,
349
+ suggested_checks=_checks("open the Preflight findings", "resolve every BLOCKER", "re-run Inspect/Preflight"),
350
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
351
+ ),
352
+ "PROFILE_CONFIG_MISSING": _spec(
353
+ stage="configuration", severity=SEVERITY_BLOCKER,
354
+ summary="Profile configuration is missing.",
355
+ description="The Pack did not record the Profile configuration that the target needs.",
356
+ expected="the Pack records Profile configuration",
357
+ impact="The new Profile could not be fully configured.",
358
+ can_continue=False,
359
+ suggested_checks=_checks("inspect the Profile configuration files", "re-export the Pack with configuration"),
360
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
361
+ ),
362
+ "PROFILE_FILE_EXISTS": _spec(
363
+ stage="configuration", severity=SEVERITY_BLOCKER,
364
+ summary="Target Profile file already exists.",
365
+ description="A configuration file the Pack needs to write already exists in the target Profile.",
366
+ expected="the target Profile is empty or overwrite is confirmed",
367
+ impact="The new Profile could not be fully configured.",
368
+ can_continue=False,
369
+ suggested_checks=_checks("inspect the conflicting file", "choose a new Profile or confirm overwrite"),
370
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_TARGET_PROFILE,
371
+ ),
372
+ "PLUGIN_TARGET_EXISTS": _spec(
373
+ stage="configuration", severity=SEVERITY_BLOCKER,
374
+ summary="Plugin target directory already exists.",
375
+ description="The plugin install location already contains content.",
376
+ expected="the plugin target directory is available",
377
+ impact="The plugin could not be placed into the target Profile.",
378
+ can_continue=False,
379
+ suggested_checks=_checks("inspect the plugin target directory", "choose a new Profile or confirm overwrite"),
380
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_TARGET_PROFILE,
381
+ ),
382
+ # --- Embedded install ---
383
+ "ARTIFACT_PATH_INVALID": _spec(
384
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
385
+ summary="Embedded artifact path is invalid.",
386
+ description="The embedded artifact path recorded in the Pack is unsafe or invalid.",
387
+ expected="the artifact path is safe and valid",
388
+ impact="An embedded package was not installed.",
389
+ can_continue=False,
390
+ suggested_checks=_checks("verify the artifact path", "re-export the Pack with a valid artifact"),
391
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
392
+ ),
393
+ "ARTIFACT_PATH_MISSING": _spec(
394
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
395
+ summary="Embedded artifact path is missing.",
396
+ description="The Pack declares an embedded artifact but does not record where it is.",
397
+ expected="the artifact path is recorded",
398
+ impact="An embedded package was not installed.",
399
+ can_continue=False,
400
+ suggested_checks=_checks("verify the Pack artifact records", "re-export the Pack"),
401
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
402
+ ),
403
+ "ARTIFACT_MEMBER_DUPLICATE": _spec(
404
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
405
+ summary="Embedded artifact contains a duplicate member.",
406
+ description="The embedded tarball contains the same logical member more than once.",
407
+ expected="every archive member appears once",
408
+ impact="An embedded package was not installed.",
409
+ can_continue=False,
410
+ suggested_checks=_checks("verify the embedded tarball", "re-export the Pack"),
411
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
412
+ ),
413
+ "ARTIFACT_MEMBER_TYPE_INVALID": _spec(
414
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
415
+ summary="Embedded artifact member type is invalid.",
416
+ description="The embedded tarball contains an unsupported member type (for example a hardlink or symlink).",
417
+ expected="all archive members are regular files or directories",
418
+ impact="An embedded package was not installed.",
419
+ can_continue=False,
420
+ suggested_checks=_checks("verify the embedded tarball members", "re-export the Pack"),
421
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
422
+ ),
423
+ "ARTIFACT_MEMBER_READ_ERROR": _spec(
424
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
425
+ summary="Embedded artifact member cannot be read.",
426
+ description="A member of the embedded tarball could not be read from the archive.",
427
+ expected="all archive members are readable",
428
+ impact="An embedded package was not installed.",
429
+ can_continue=False,
430
+ suggested_checks=_checks("verify the embedded tarball integrity", "re-export the Pack"),
431
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
432
+ ),
433
+ "ARTIFACT_UNPACK_FAILED": _spec(
434
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
435
+ summary="Embedded artifact unpack failed.",
436
+ description="The embedded tarball could not be unpacked into the temporary Profile.",
437
+ expected="the embedded artifact unpacks successfully",
438
+ impact="An embedded package was not installed.",
439
+ can_continue=False,
440
+ suggested_checks=_checks("verify the embedded tarball", "check disk space and permissions"),
441
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
442
+ ),
443
+ "EMBEDDED_NPM_UNAVAILABLE": _spec(
444
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
445
+ summary="npm is unavailable for embedded install.",
446
+ description="npm could not be located or executed while installing embedded dependencies.",
447
+ expected="npm is available",
448
+ impact="Embedded plugin dependencies were not installed.",
449
+ can_continue=False,
450
+ suggested_checks=_checks("verify npm is on PATH", "reinstall Node/npm and retry"),
451
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
452
+ ),
453
+ "EMBEDDED_NPM_FAILED": _spec(
454
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
455
+ summary="Embedded npm install failed.",
456
+ description="npm exited with a non-zero code while installing embedded dependencies.",
457
+ expected="npm install exits 0",
458
+ impact="Embedded plugin dependencies were not installed.",
459
+ can_continue=False,
460
+ suggested_checks=_checks("inspect the npm stdout/stderr evidence", "check network and registry access"),
461
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
462
+ ),
463
+ "EMBEDDED_DEPENDENCIES_TIMEOUT": _spec(
464
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
465
+ summary="Embedded dependency install timed out.",
466
+ description="Installing embedded dependencies exceeded the configured timeout.",
467
+ expected="embedded dependency install completes in time",
468
+ impact="Embedded plugin dependencies were not installed.",
469
+ can_continue=False,
470
+ suggested_checks=_checks("check network latency", "retry with a longer timeout"),
471
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
472
+ ),
473
+ "EMBEDDED_DEPENDENCIES_INSTALL_FAILED": _spec(
474
+ stage="embedded-install", severity=SEVERITY_BLOCKER,
475
+ summary="Embedded dependency install failed.",
476
+ description="Installing an embedded plugin artifact failed with a non-zero exit code.",
477
+ expected="install a valid embedded artifact",
478
+ impact="Import stopped during embedded-install; the temporary Profile was cleaned.",
479
+ can_continue=False,
480
+ suggested_checks=_checks("verify the embedded artifact SHA-256 against the Pack lock", "confirm the artifact is a valid npm tarball", "check the npm stdout/stderr evidence"),
481
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
482
+ ),
483
+ # --- Reference / network install ---
484
+ "PLUGIN_SOURCE_MISSING": _spec(
485
+ stage="reference-install", severity=SEVERITY_BLOCKER,
486
+ summary="Plugin source is missing.",
487
+ description="The reference-only plugin source recorded in the Pack cannot be resolved.",
488
+ expected="the plugin source is resolvable",
489
+ impact="A reference-only package was not installed.",
490
+ can_continue=False,
491
+ suggested_checks=_checks("provide the exact reference source", "verify the source still exists", "retry Import"),
492
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
493
+ ),
494
+ "PLUGIN_SOURCE_UNSAFE": _spec(
495
+ stage="reference-install", severity=SEVERITY_BLOCKER,
496
+ summary="Plugin source is unsafe.",
497
+ description="The reference-only plugin source uses an unsafe or unsupported source type.",
498
+ expected="the plugin source is safe and supported",
499
+ impact="The plugin was not installed from the unsafe source.",
500
+ can_continue=False,
501
+ suggested_checks=_checks("review the source type", "choose an embedded artifact or a supported source"),
502
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_TARGET_PROFILE,
503
+ ),
504
+ "PLUGIN_INSTALL_FAILED": _spec(
505
+ stage="reference-install", severity=SEVERITY_BLOCKER,
506
+ summary="Plugin install failed.",
507
+ description="Installing the reference-only plugin failed.",
508
+ expected="the plugin installs successfully",
509
+ impact="A reference-only package was not installed.",
510
+ can_continue=False,
511
+ suggested_checks=_checks("inspect the install stdout/stderr", "verify the source and version", "retry Import"),
512
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
513
+ ),
514
+ "PLUGIN_MISSING": _spec(
515
+ stage="reference-install", severity=SEVERITY_BLOCKER,
516
+ summary="Plugin is missing after install.",
517
+ description="The plugin is not present after the install step.",
518
+ expected="the plugin is present after install",
519
+ impact="The plugin could not be placed into the target Profile.",
520
+ can_continue=False,
521
+ suggested_checks=_checks("verify the install command completed", "inspect the plugin directory"),
522
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
523
+ ),
524
+ "PLUGIN_VERSION_MISMATCH": _spec(
525
+ stage="version-confirmation", severity=SEVERITY_BLOCKER,
526
+ summary="Installed plugin version does not match the Pack.",
527
+ description="The installed plugin version does not satisfy the Pack resolution identity.",
528
+ expected="the installed package version matches the Pack resolution",
529
+ impact="Import stopped during version confirmation.",
530
+ can_continue=False,
531
+ suggested_checks=_checks("provide an artifact or source matching the Pack installation target identity", "check recorded resolution and runtime evidence separately"),
532
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
533
+ ),
534
+ "REQUIRED_ARTIFACT_MISSING": _spec(
535
+ stage="reference-install", severity=SEVERITY_BLOCKER,
536
+ summary="Required artifact is missing.",
537
+ description="A required plugin artifact recorded in the Pack is not present and cannot be resolved.",
538
+ expected="every required artifact is available",
539
+ impact="Import cannot continue without the required artifact.",
540
+ can_continue=False,
541
+ suggested_checks=_checks("verify the required artifact record", "provide the artifact or re-export the Pack"),
542
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
543
+ ),
544
+ "NETWORK_SOURCE_UNSUPPORTED": _spec(
545
+ stage="network-install", severity=SEVERITY_BLOCKER,
546
+ summary="Network source type is unsupported.",
547
+ description="The Pack records a network source type the installed Core does not support.",
548
+ expected="the network source type is supported",
549
+ impact="The network package was not installed.",
550
+ can_continue=False,
551
+ suggested_checks=_checks("verify the Pack source type", "use an embedded artifact instead"),
552
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
553
+ ),
554
+ "NETWORK_INSTALL_FAILED": _spec(
555
+ stage="network-install", severity=SEVERITY_BLOCKER,
556
+ summary="Network install failed.",
557
+ description="npm install of the recorded network package failed.",
558
+ expected="npm install exits 0",
559
+ impact="The network package was not installed into the temporary Profile.",
560
+ can_continue=False,
561
+ suggested_checks=_checks("check npm registry and network reachability", "verify the recorded package version exists", "retry"),
562
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
563
+ ),
564
+ "NETWORK_INSTALL_TIMEOUT": _spec(
565
+ stage="network-install", severity=SEVERITY_BLOCKER,
566
+ summary="Network install timed out.",
567
+ description="npm install of the recorded network package exceeded the timeout.",
568
+ expected="npm install completes in time",
569
+ impact="The network package was not installed into the temporary Profile.",
570
+ can_continue=False,
571
+ suggested_checks=_checks("check network latency", "retry with a longer timeout", "or provide an embedded artifact"),
572
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
573
+ ),
574
+ "NETWORK_PACKAGE_MISSING": _spec(
575
+ stage="network-install", severity=SEVERITY_BLOCKER,
576
+ summary="Network package is missing.",
577
+ description="The recorded network package could not be found after install.",
578
+ expected="the network package is present after install",
579
+ impact="The network package was not installed into the temporary Profile.",
580
+ can_continue=False,
581
+ suggested_checks=_checks("verify the package name and version", "check the registry response"),
582
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
583
+ ),
584
+ # --- Overwrite / delete ---
585
+ "OVERWRITE_CONFIRMATION_REQUIRED": _spec(
586
+ stage="planning", severity=SEVERITY_BLOCKER,
587
+ summary="Overwrite requires explicit confirmation.",
588
+ description="Importing over an existing Profile requires the user to confirm before anything is written.",
589
+ expected="the user confirms the overwrite",
590
+ impact="No Profile was written.",
591
+ can_continue=False,
592
+ suggested_checks=_checks("confirm the overwrite target", "retry after confirmation"),
593
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_TARGET_PROFILE,
594
+ ),
595
+ "OVERWRITE_MODE_REQUIRED": _spec(
596
+ stage="planning", severity=SEVERITY_BLOCKER,
597
+ summary="Overwrite mode is required.",
598
+ description="The Import targets an existing Profile but overwrite mode was not selected.",
599
+ expected="overwrite mode is selected for an existing target",
600
+ impact="No Profile was written.",
601
+ can_continue=False,
602
+ suggested_checks=_checks("select overwrite mode", "or choose a new Profile name"),
603
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_TARGET_PROFILE,
604
+ ),
605
+ "DELETE_CONFIRMATION_REQUIRED": _spec(
606
+ stage="delete", severity=SEVERITY_BLOCKER,
607
+ summary="Delete requires explicit confirmation.",
608
+ description="Deleting a Profile is destructive and requires the user to confirm.",
609
+ expected="the user confirms the deletion",
610
+ impact="The Profile was not deleted.",
611
+ can_continue=False,
612
+ suggested_checks=_checks("confirm the deletion", "retry after confirmation"),
613
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_TARGET_PROFILE,
614
+ ),
615
+ "PROFILE_NOT_FOUND": _spec(
616
+ stage="delete", severity=SEVERITY_BLOCKER,
617
+ summary="Profile not found.",
618
+ description="The Profile selected for deletion does not exist.",
619
+ expected="the Profile exists",
620
+ impact="The Profile was not deleted.",
621
+ can_continue=False,
622
+ suggested_checks=_checks("verify the Profile name", "refresh the Profile list"),
623
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
624
+ ),
625
+ "PROFILE_DELETE_FAILED": _spec(
626
+ stage="delete", severity=SEVERITY_BLOCKER,
627
+ summary="Profile deletion failed.",
628
+ description="Deleting the Profile failed (for example an OS error).",
629
+ expected="the Profile is deleted",
630
+ impact="The Profile was not deleted.",
631
+ can_continue=False,
632
+ suggested_checks=_checks("check file permissions and locks", "inspect the delete evidence"),
633
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TARGET_PROFILE,
634
+ ),
635
+ "IMPORT_FAILED": _spec(
636
+ stage="commit", severity=SEVERITY_BLOCKER,
637
+ summary="Import failed.",
638
+ description="The Import operation failed; the target and temporary Profiles are cleaned.",
639
+ expected="Import commits a prepared Profile",
640
+ impact="Import stopped; no half-written Profile remains.",
641
+ can_continue=False,
642
+ suggested_checks=_checks("open the failure diagnostic", "correct the reported item", "retry Import"),
643
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
644
+ ),
645
+
646
+ # --- Verify / runtime ---
647
+ "RUNNER_SPAWN_FAILED": _spec(
648
+ stage="runtime", severity=SEVERITY_BLOCKER,
649
+ summary="Runtime command could not start.",
650
+ description="The runtime command could not be spawned during Verify.",
651
+ expected="the runtime command starts and prints a ready URL",
652
+ impact="DSH runtime could not be started for verification.",
653
+ can_continue=False,
654
+ suggested_checks=_checks("inspect the spawn error", "verify the runtime command and PATH"),
655
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
656
+ ),
657
+ "RUNNER_READY_REGEX_INVALID": _spec(
658
+ stage="runtime", severity=SEVERITY_BLOCKER,
659
+ summary="Runtime readyRegex is invalid.",
660
+ description="The Verify runner readyRegex is not a valid regular expression.",
661
+ expected="the readyRegex compiles",
662
+ impact="Runtime readiness cannot be detected.",
663
+ can_continue=False,
664
+ suggested_checks=_checks("correct the readyRegex in the Verify configuration", "re-run Verify"),
665
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TEMPORARY_PROFILE,
666
+ ),
667
+ "PROFILE_PATCH_ADAPTATION_FAILED": _spec(
668
+ stage="configuration", severity=SEVERITY_BLOCKER,
669
+ summary="无法证明恢复配置与官方 Loader 配置等价。",
670
+ description="官方桌面配置适配器失败、证据不完整,或适配前后配置身份不一致。",
671
+ expected="原 patch 保留,只有官方完整 composition 等价时才能提交适配结果",
672
+ impact="本次新目标不会提交,源 Profile 不变。", can_continue=False,
673
+ suggested_checks=_checks("检查官方解析器与完整错误证据", "保留原 Crate,不删除必需插件或绕过等价校验"),
674
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
675
+ ),
676
+ "PROFILE_PATCH_ADAPTER_COMMAND_INVALID": _spec(
677
+ stage="planning", severity=SEVERITY_BLOCKER,
678
+ summary="桌面配置适配器命令无效。",
679
+ description="受信任环境中的适配器配置不是有效 JSON argv 数组。",
680
+ expected="适配器来自当前受信任桌面运行时", impact="导入计划停止,未修改 Profile。", can_continue=False,
681
+ suggested_checks=_checks("检查桌面桥接配置,不从 Crate 读取命令"),
682
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
683
+ ),
684
+ "PACKAGE_MANAGER_REQUIRED": _spec(
685
+ stage="planning", severity=SEVERITY_BLOCKER,
686
+ summary="新建 Profile 需要正式包管理器。",
687
+ description="当前运行时没有提供受信任的 pnpm,不能通过复制目录伪装为正式安装。",
688
+ expected="使用官方随附 pnpm 或显式配置受信任的命令", impact="新 Profile 未创建。", can_continue=False,
689
+ suggested_checks=_checks("使用官方桌面,或检查 DSH_PACK_PNPM_COMMAND"),
690
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
691
+ ),
692
+ "RUNNER_EXITED_BEFORE_READY": _spec(
693
+ stage="runtime", severity=SEVERITY_BLOCKER,
694
+ summary="Runtime exited before ready.",
695
+ description="The runtime process exited before printing a ready URL.",
696
+ expected="the runtime process stays alive and prints a ready URL",
697
+ impact="The imported Profile cannot boot in the target DSH runtime.",
698
+ can_continue=False,
699
+ suggested_checks=_checks("read the runtime stdout/stderr evidence", "verify the Profile composition and dependencies"),
700
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
701
+ ),
702
+ "RUNNER_READY_TIMEOUT": _spec(
703
+ stage="runtime", severity=SEVERITY_BLOCKER,
704
+ summary="Runtime ready timeout.",
705
+ description="The runtime did not print a ready URL before the configured timeout.",
706
+ expected="the runtime prints a ready URL within the configured timeout",
707
+ impact="DSH readiness could not be confirmed within the timeout.",
708
+ can_continue=False,
709
+ suggested_checks=_checks("inspect the runtime logs", "verify the port is free", "increase the timeout or fix boot"),
710
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
711
+ ),
712
+ "HEADLESS_RUNNER_FAILED": _spec(
713
+ stage="runtime", severity=SEVERITY_BLOCKER,
714
+ summary="Headless runtime failed.",
715
+ description="The headless runtime command failed to complete or exited with a failure.",
716
+ expected="the headless runtime command completes successfully",
717
+ impact="The imported Profile could not be verified in headless mode.",
718
+ can_continue=False,
719
+ suggested_checks=_checks("inspect the headless stdout/stderr", "verify the Profile composition"),
720
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
721
+ ),
722
+ "WEB_HTTP_STATUS_MISMATCH": _spec(
723
+ stage="runtime", severity=SEVERITY_BLOCKER,
724
+ summary="Web HTTP status does not match.",
725
+ description="The web endpoint returned an HTTP status that does not match the expected status.",
726
+ expected="the web endpoint returns the expected HTTP status",
727
+ impact="Web readiness could not be confirmed.",
728
+ can_continue=False,
729
+ suggested_checks=_checks("check the returned status code", "verify the web server started"),
730
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
731
+ ),
732
+ "WEB_HTTP_UNREACHABLE": _spec(
733
+ stage="runtime", severity=SEVERITY_BLOCKER,
734
+ summary="Web endpoint is unreachable.",
735
+ description="The web endpoint did not respond while the runtime is running.",
736
+ expected="the web endpoint is reachable",
737
+ impact="Web readiness could not be confirmed.",
738
+ can_continue=False,
739
+ suggested_checks=_checks("verify the URL and port", "check firewall and process state"),
740
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
741
+ ),
742
+ "WEB_PORT_NOT_OWNED": _spec(
743
+ stage="runtime", severity=SEVERITY_BLOCKER,
744
+ summary="Web port is not owned by the runtime.",
745
+ description="The expected web port is not owned by the started runtime process.",
746
+ expected="the runtime process owns the configured port",
747
+ impact="Web readiness could not be confirmed on the expected port.",
748
+ can_continue=False,
749
+ suggested_checks=_checks("check which process owns the port", "free the port or use a different port", "restart DSH"),
750
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_ENVIRONMENT,
751
+ ),
752
+ "RESTART_PORT_NOT_RELEASED": _spec(
753
+ stage="restart", severity=SEVERITY_BLOCKER,
754
+ summary="Port was not released after restart.",
755
+ description="The old DSH process did not release the port before the new process started.",
756
+ expected="the old process releases the port",
757
+ impact="The restarted DSH could not bind the expected port.",
758
+ can_continue=False,
759
+ suggested_checks=_checks("verify the old process exited", "free the port manually", "restart again"),
760
+ repair_level=REPAIR_LEVEL_CONFIRMATION, write_scope=WRITE_SCOPE_ENVIRONMENT,
761
+ ),
762
+ "PROBE_FAILED": _spec(
763
+ stage="probe", severity=SEVERITY_BLOCKER,
764
+ summary="Runtime probe failed.",
765
+ description="A runtime capability probe command failed to complete or exited with a failure.",
766
+ expected="the probe command completes successfully",
767
+ impact="The runtime capability could not be verified.",
768
+ can_continue=False,
769
+ suggested_checks=_checks("inspect the probe stdout/stderr", "verify the probed capability"),
770
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
771
+ ),
772
+ "PLUGIN_SMOKE_FAILED": _spec(
773
+ stage="plugin-smoke", severity=SEVERITY_BLOCKER,
774
+ summary="Plugin smoke test failed.",
775
+ description="A plugin-specific smoke test failed against the runtime.",
776
+ expected="the plugin smoke test passes",
777
+ impact="The plugin business function could not be verified.",
778
+ can_continue=False,
779
+ suggested_checks=_checks("inspect the smoke test output", "verify the plugin is active", "check plugin external dependencies"),
780
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
781
+ ),
782
+ # --- Preflight findings ---
783
+ "SCHEMA_ERROR": _spec(
784
+ stage="preflight", severity=SEVERITY_BLOCKER,
785
+ summary="Pack schema is unsupported.",
786
+ description="The Pack manifest schema version is not supported by this Core.",
787
+ expected="the Pack schema version is supported",
788
+ impact="The Pack cannot be interpreted.",
789
+ can_continue=False,
790
+ suggested_checks=_checks("verify the Pack schemaVersion", "use a matching DSH Crate version"),
791
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
792
+ ),
793
+ "FORMAT_ERROR": _spec(
794
+ stage="preflight", severity=SEVERITY_BLOCKER,
795
+ summary="Pack format is invalid.",
796
+ description="The Pack container or required members are invalid.",
797
+ expected="the Pack format is valid",
798
+ impact="The Pack cannot be read.",
799
+ can_continue=False,
800
+ suggested_checks=_checks("verify the Pack file", "re-export the Pack"),
801
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
802
+ ),
803
+ "INTEGRITY_FILE_MISSING": _spec(
804
+ stage="preflight", severity=SEVERITY_BLOCKER,
805
+ summary="Integrity record is missing.",
806
+ description="A required integrity record is missing from the Pack.",
807
+ expected="all integrity records are present",
808
+ impact="The Pack integrity cannot be verified.",
809
+ can_continue=False,
810
+ suggested_checks=_checks("verify the Pack integrity records", "re-export the Pack"),
811
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
812
+ ),
813
+ "INTEGRITY_FILE_SET_MISMATCH": _spec(
814
+ stage="preflight", severity=SEVERITY_BLOCKER,
815
+ summary="Integrity file set does not match.",
816
+ description="The set of files covered by the integrity records does not match the Pack contents.",
817
+ expected="the integrity file set matches the Pack contents",
818
+ impact="The Pack integrity cannot be verified.",
819
+ can_continue=False,
820
+ suggested_checks=_checks("verify the Pack file set", "re-export the Pack"),
821
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
822
+ ),
823
+ "HASH_OR_INTEGRITY_ERROR": _spec(
824
+ stage="preflight", severity=SEVERITY_BLOCKER,
825
+ summary="Pack hash or integrity check failed.",
826
+ description="A declared hash or integrity record does not match the Pack contents.",
827
+ expected="every declared hash matches the Pack contents",
828
+ impact="The Pack is not accepted as-is.",
829
+ can_continue=False,
830
+ suggested_checks=_checks("verify the affected hash", "re-export the Pack"),
831
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
832
+ ),
833
+ "HASH_MISMATCH": _spec(
834
+ stage="preflight", severity=SEVERITY_BLOCKER,
835
+ summary="Pack hash mismatch.",
836
+ description="A specific hash record does not match the Pack contents.",
837
+ expected="the hash matches the Pack contents",
838
+ impact="The Pack is not accepted as-is.",
839
+ can_continue=False,
840
+ suggested_checks=_checks("verify the affected hash", "re-export the Pack"),
841
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
842
+ ),
843
+ "CONFLICT_BUNDLE_COMPOSITION": _spec(
844
+ stage="composition", severity=SEVERITY_BLOCKER,
845
+ summary="Bundle composition conflict.",
846
+ description="The Bundle composition duplicates or conflicts on a Loader row.",
847
+ expected="every Loader row is unique and composable",
848
+ impact="The Profile Bundle composition cannot boot.",
849
+ can_continue=False,
850
+ suggested_checks=_checks("review the Bundle order", "remove the conflicting duplicate", "re-export the Pack"),
851
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_TARGET_PROFILE,
852
+ ),
853
+ "CONFLICT_DUPLICATE_ARTIFACT": _spec(
854
+ stage="composition", severity=SEVERITY_BLOCKER,
855
+ summary="Duplicate plugin artifact.",
856
+ description="Two plugins resolve to the same artifact in the composition.",
857
+ expected="every plugin resolves to a distinct artifact",
858
+ impact="The Profile composition is ambiguous.",
859
+ can_continue=False,
860
+ suggested_checks=_checks("review the plugin identity records", "re-export the Pack"),
861
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
862
+ ),
863
+ "CONFLICT_DUPLICATE_BUNDLE_ORDER": _spec(
864
+ stage="composition", severity=SEVERITY_BLOCKER,
865
+ summary="Duplicate Bundle order.",
866
+ description="A Bundle appears more than once in the Bundle order.",
867
+ expected="every Bundle appears exactly once in order",
868
+ impact="The Bundle order is ambiguous.",
869
+ can_continue=False,
870
+ suggested_checks=_checks("review the Bundle list and order", "re-export the Pack"),
871
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
872
+ ),
873
+ "PACKAGE_IDENTITY_DRIFT": _spec(
874
+ stage="version-confirmation", severity=SEVERITY_BLOCKER,
875
+ summary="Package identity drift.",
876
+ description="The Pack identity (SPEC/RESOLUTION) drifts from the actual package source or version.",
877
+ expected="the Pack identity matches the resolved package",
878
+ impact="The Pack cannot be imported with the recorded identity.",
879
+ can_continue=False,
880
+ suggested_checks=_checks("verify SPEC and RESOLUTION records", "re-export the Pack from the installed state"),
881
+ repair_level=REPAIR_LEVEL_SAFE, write_scope=WRITE_SCOPE_NONE,
882
+ ),
883
+ "ENVIRONMENT_OS_MISMATCH": _spec(
884
+ stage="environment", severity=SEVERITY_WARNING,
885
+ summary="Operating system differs from the source.",
886
+ description="The target operating system differs from the OS recorded at Export.",
887
+ expected="the target OS matches the source (or the difference is acceptable)",
888
+ impact="Some behaviors may differ from the source environment.",
889
+ can_continue=True,
890
+ suggested_checks=_checks("review the OS difference", "verify the target is supported"),
891
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
892
+ ),
893
+ "ENVIRONMENT_VERSION_MISMATCH": _spec(
894
+ stage="environment", severity=SEVERITY_WARNING,
895
+ summary="Environment version differs from the source.",
896
+ description="A runtime version (Node, DSH) differs from the version recorded at Export.",
897
+ expected="the target versions match the source (or the difference is acceptable)",
898
+ impact="Some behaviors may differ from the source environment.",
899
+ can_continue=True,
900
+ suggested_checks=_checks("review the version difference", "confirm it is not a breaking difference"),
901
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
902
+ ),
903
+ "ENVIRONMENT_EXACT_MATCH": _spec(
904
+ stage="environment", severity=SEVERITY_INFO,
905
+ summary="Environment matches the source.",
906
+ description="The target environment matches the environment recorded at Export.",
907
+ expected="the environment matches",
908
+ impact="No environment risk detected.",
909
+ can_continue=True,
910
+ suggested_checks=_checks("none required"),
911
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
912
+ ),
913
+ "VERSION_MISSING": _spec(
914
+ stage="environment", severity=SEVERITY_WARNING,
915
+ summary="A recorded version is missing.",
916
+ description="A version record required for an environment comparison is missing.",
917
+ expected="all version records are present",
918
+ impact="The environment comparison is incomplete.",
919
+ can_continue=True,
920
+ suggested_checks=_checks("verify the Pack environment records", "re-export the Pack with version data"),
921
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
922
+ ),
923
+ "NETWORK_INSTALL_REQUIRED": _spec(
924
+ stage="network-install", severity=SEVERITY_WARNING,
925
+ summary="Network install will be required.",
926
+ description="Import will need to install a plugin from the network on the target machine.",
927
+ expected="network access is available (or an embedded artifact is supplied)",
928
+ impact="Import may fail without network access.",
929
+ can_continue=True,
930
+ suggested_checks=_checks("confirm network access on the target", "prefer embedded artifacts for offline targets"),
931
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
932
+ ),
933
+ "OPTIONAL_PLUGIN_MISSING": _spec(
934
+ stage="reference-install", severity=SEVERITY_WARNING,
935
+ summary="Optional plugin is missing.",
936
+ description="An optional plugin recorded in the Pack is not available on the target.",
937
+ expected="optional plugins resolve (or are intentionally skipped)",
938
+ impact="The optional plugin will be skipped.",
939
+ can_continue=True,
940
+ suggested_checks=_checks("confirm the optional plugin is not required", "provide the optional plugin source"),
941
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
942
+ ),
943
+ "OPTIONAL_ARTIFACT_MISSING": _spec(
944
+ stage="reference-install", severity=SEVERITY_WARNING,
945
+ summary="Optional artifact is missing.",
946
+ description="An optional plugin artifact is missing on the target.",
947
+ expected="optional artifacts resolve (or are intentionally skipped)",
948
+ impact="The optional artifact will be skipped.",
949
+ can_continue=True,
950
+ suggested_checks=_checks("confirm the optional artifact is not required"),
951
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
952
+ ),
953
+ "SECRET_MISSING": _spec(
954
+ stage="planning", severity=SEVERITY_WARNING,
955
+ summary="Required Secret is missing.",
956
+ description="The Pack declares a required Secret name that is not present in the target environment.",
957
+ expected="every required Secret is present",
958
+ impact="The restored Profile may not work until the Secret is provided.",
959
+ can_continue=True,
960
+ suggested_checks=_checks("provide the required Secret in the target environment", "confirm the Secret name matches the Pack"),
961
+ repair_level=REPAIR_LEVEL_READ_ONLY, write_scope=WRITE_SCOPE_NONE,
962
+ ),
963
+ }
964
+ # Preflight may report PLUGIN_MISSING / PLUGIN_VERSION_MISMATCH as WARNING
965
+ # findings; when the same code appears as an Import failure it is a BLOCKER.
966
+ for _alias in ("PLUGIN_MISSING", "PLUGIN_VERSION_MISMATCH"):
967
+ _base = DIAGNOSTIC_SPECS[_alias]
968
+ DIAGNOSTIC_SPECS[_alias] = {
969
+ **_base,
970
+ "description": _base["description"]
971
+ + " Preflight may report this code as a WARNING finding; as an Import failure it is a BLOCKER.",
972
+ }
973
+ del _alias, _base
974
+
975
+ # ---------------------------------------------------------------------------
976
+ # Envelope
977
+ # ---------------------------------------------------------------------------
978
+
979
+
980
+ def envelope_fields(*, operation: str, status: str, operation_id: str | None = None) -> dict[str, Any]:
981
+ """Return the versioned diagnostic envelope fields.
982
+
983
+ Every diagnostic produced by DSH Crate carries these fields so a
984
+ Troubleshooting Skill can decide whether it understands the diagnostic.
985
+ """
986
+ envelope = {
987
+ "producer": PRODUCER,
988
+ "crateVersion": CRATE_VERSION,
989
+ "diagnosticSchemaVersion": DIAGNOSTIC_SCHEMA_VERSION,
990
+ "operation": operation,
991
+ "status": status,
992
+ }
993
+ if operation_id:
994
+ envelope["operationId"] = operation_id
995
+ return envelope
996
+
997
+
998
+ def decorate_issue(
999
+ issue: Mapping[str, Any],
1000
+ *,
1001
+ operation: str,
1002
+ status: str,
1003
+ operation_id: str | None = None,
1004
+ ) -> dict[str, Any]:
1005
+ """Merge the versioned envelope into a single-issue diagnostic (additive)."""
1006
+ merged = dict(issue)
1007
+ for key, value in envelope_fields(operation=operation, status=status, operation_id=operation_id).items():
1008
+ merged.setdefault(key, value)
1009
+ return merged
1010
+
1011
+
1012
+ # ---------------------------------------------------------------------------
1013
+ # Registry access
1014
+ # ---------------------------------------------------------------------------
1015
+
1016
+
1017
+ def spec(code: str) -> dict[str, Any] | None:
1018
+ """Return the registry entry for a diagnostic code, or None when unknown."""
1019
+ return DIAGNOSTIC_SPECS.get(code)
1020
+
1021
+
1022
+ def known(code: str) -> bool:
1023
+ """Whether a diagnostic code is registered."""
1024
+ return code in DIAGNOSTIC_SPECS
1025
+
1026
+
1027
+ def classify(issue: Mapping[str, Any]) -> dict[str, Any]:
1028
+ """Resolve an issue diagnostic against the registry.
1029
+
1030
+ Returns the matched registry fields plus ``known``. When the code is not
1031
+ registered the result marks it unknown and forbids automated repair.
1032
+ """
1033
+ code = issue.get("code")
1034
+ if not isinstance(code, str):
1035
+ return {
1036
+ "known": False,
1037
+ "code": None,
1038
+ "reason": "diagnostic carries no string code; do not guess",
1039
+ "repairLevel": REPAIR_LEVEL_FORBIDDEN,
1040
+ "writeScope": WRITE_SCOPE_NONE,
1041
+ "verifyAfter": False,
1042
+ }
1043
+ entry = spec(code)
1044
+ if entry is None:
1045
+ return {
1046
+ "known": False,
1047
+ "code": code,
1048
+ "reason": "diagnostic code is not in the registry; update the Skill before automated repair",
1049
+ "repairLevel": REPAIR_LEVEL_FORBIDDEN,
1050
+ "writeScope": WRITE_SCOPE_NONE,
1051
+ "verifyAfter": False,
1052
+ }
1053
+ return {
1054
+ "known": True,
1055
+ "code": code,
1056
+ **entry,
1057
+ }
1058
+
1059
+
1060
+ # ---------------------------------------------------------------------------
1061
+ # Compatibility gate
1062
+ # ---------------------------------------------------------------------------
1063
+
1064
+
1065
+ def _parse_semver(value: str) -> tuple[int, int, int] | None:
1066
+ match = re.match(r"^\s*v?(\d+)\.(\d+)\.(\d+)", value)
1067
+ if not match:
1068
+ return None
1069
+ return int(match.group(1)), int(match.group(2)), int(match.group(3))
1070
+
1071
+
1072
+ def _clause_match(version: tuple[int, int, int], op: str, target: tuple[int, int, int]) -> bool:
1073
+ if op == ">=":
1074
+ return version >= target
1075
+ if op == ">":
1076
+ return version > target
1077
+ if op == "<=":
1078
+ return version <= target
1079
+ if op == "<":
1080
+ return version < target
1081
+ return version == target
1082
+
1083
+
1084
+ def version_in_range(version: str, range_spec: str) -> bool:
1085
+ """Whether a version satisfies a simple space-separated range (>=, >, <=, <, =, ==)."""
1086
+ if not range_spec or range_spec.strip() == "*":
1087
+ return True
1088
+ parsed = _parse_semver(version)
1089
+ if parsed is None:
1090
+ return False
1091
+ for clause in range_spec.split():
1092
+ op = ""
1093
+ target_text = clause
1094
+ for candidate in (">=", "<=", ">", "<", "==", "="):
1095
+ if clause.startswith(candidate):
1096
+ op = candidate
1097
+ target_text = clause[len(candidate):]
1098
+ break
1099
+ if not op:
1100
+ op = "="
1101
+ target = _parse_semver(target_text)
1102
+ if target is None:
1103
+ return False
1104
+ if not _clause_match(parsed, op, target):
1105
+ return False
1106
+ return True
1107
+
1108
+
1109
+ def compat_status(
1110
+ *,
1111
+ skill_version: str,
1112
+ dsh_crate_range: str,
1113
+ supported_schemas: list[int],
1114
+ crate_version: str,
1115
+ diagnostic_schema_version: int,
1116
+ ) -> dict[str, Any]:
1117
+ """Return the compatibility result for a Skill against a diagnostic.
1118
+
1119
+ - FULL: same Crate generation, supported schema -> normal troubleshooting.
1120
+ - COMPATIBLE: older Crate inside the supported range -> compatibility rules.
1121
+ - UNSUPPORTED: Crate outside the range or schema unsupported -> never guess.
1122
+ """
1123
+ schema_supported = diagnostic_schema_version in (supported_schemas or [])
1124
+ version_in = version_in_range(crate_version, dsh_crate_range)
1125
+ if not schema_supported or not version_in:
1126
+ reason_parts = []
1127
+ if not version_in:
1128
+ reason_parts.append(
1129
+ f"this diagnostic was produced by DSH Crate {crate_version}, which is outside the supported range {dsh_crate_range}"
1130
+ )
1131
+ if not schema_supported:
1132
+ reason_parts.append(
1133
+ f"diagnostic schema v{diagnostic_schema_version} is not supported (supported: {supported_schemas or []})"
1134
+ )
1135
+ return {
1136
+ "status": "UNSUPPORTED",
1137
+ "reason": "; ".join(reason_parts),
1138
+ "skillVersion": skill_version,
1139
+ "crateVersion": crate_version,
1140
+ "diagnosticSchemaVersion": diagnostic_schema_version,
1141
+ "schemaSupported": schema_supported,
1142
+ "versionInRange": version_in,
1143
+ "canProceed": False,
1144
+ }
1145
+ skill = _parse_semver(skill_version)
1146
+ crate = _parse_semver(crate_version)
1147
+ same_generation = (
1148
+ skill is not None and crate is not None and skill[0] == crate[0] and skill[1] == crate[1]
1149
+ )
1150
+ if same_generation:
1151
+ return {
1152
+ "status": "FULL",
1153
+ "reason": "same Crate generation and supported diagnostic schema",
1154
+ "skillVersion": skill_version,
1155
+ "crateVersion": crate_version,
1156
+ "diagnosticSchemaVersion": diagnostic_schema_version,
1157
+ "schemaSupported": True,
1158
+ "versionInRange": True,
1159
+ "canProceed": True,
1160
+ }
1161
+ return {
1162
+ "status": "COMPATIBLE",
1163
+ "reason": "Crate version is inside the supported range; use compatibility rules",
1164
+ "skillVersion": skill_version,
1165
+ "crateVersion": crate_version,
1166
+ "diagnosticSchemaVersion": diagnostic_schema_version,
1167
+ "schemaSupported": True,
1168
+ "versionInRange": True,
1169
+ "canProceed": True,
1170
+ }