dsh-plugin-update 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -2,6 +2,46 @@
2
2
  import { buildPhoneNames, DEFAULT_PANEL_POLL_MS, MIN_PANEL_POLL_MS } from "./config.js";
3
3
  import { buildPhoneName, buildPhoneNames as buildPhoneNames2 } from "./config.js";
4
4
  import { manualCommand as manualCommand2 } from "./commands.js";
5
+ import {
6
+ QUEUE_INTENT_TTL_MS,
7
+ cancelEnqueuedInQueue,
8
+ emptyQueueState,
9
+ enqueueInQueue,
10
+ isHeadOfQueue,
11
+ isQueueBusy,
12
+ normalizeQueueState,
13
+ pruneExpiredIntents,
14
+ queuePositionOf,
15
+ releaseOwnerInQueue,
16
+ setOwnerIfFree,
17
+ visibleQueueFor
18
+ } from "./queue.js";
19
+ import {
20
+ BATCH_SESSION_VERSION,
21
+ batchEntryOf,
22
+ batchProgress,
23
+ batchRequestId,
24
+ createBatchSession,
25
+ emptyBatchSession,
26
+ failedKeys,
27
+ isBatchFinished,
28
+ isTerminalPhase,
29
+ markBatchEntry,
30
+ needsRestartKeys,
31
+ nextBatchKey,
32
+ normalizeBatchSession,
33
+ orderTargets,
34
+ resumeBatchSession
35
+ } from "./batch.js";
36
+ import {
37
+ CHANGELOG_FILENAME,
38
+ CHANGELOG_NEUTRAL_HINT,
39
+ CHANGELOG_NEUTRAL_LINE,
40
+ changelogForUpdate,
41
+ parseChangelog,
42
+ renderChangelogHTML,
43
+ selectChangelogEntries
44
+ } from "./changelog.js";
5
45
  const CLIENT_POLL = {
6
46
  defaultMs: DEFAULT_PANEL_POLL_MS,
7
47
  minMs: MIN_PANEL_POLL_MS
@@ -16,10 +56,44 @@ function assertPollInterval(ms) {
16
56
  return ms;
17
57
  }
18
58
  export {
59
+ BATCH_SESSION_VERSION,
60
+ CHANGELOG_FILENAME,
61
+ CHANGELOG_NEUTRAL_HINT,
62
+ CHANGELOG_NEUTRAL_LINE,
19
63
  CLIENT_POLL,
64
+ QUEUE_INTENT_TTL_MS,
20
65
  assertPollInterval,
66
+ batchEntryOf,
67
+ batchProgress,
68
+ batchRequestId,
21
69
  buildClientPhoneNames,
22
70
  buildPhoneName,
23
71
  buildPhoneNames2 as buildPhoneNames,
24
- manualCommand2 as manualCommand
72
+ cancelEnqueuedInQueue,
73
+ changelogForUpdate,
74
+ createBatchSession,
75
+ emptyBatchSession,
76
+ emptyQueueState,
77
+ enqueueInQueue,
78
+ failedKeys,
79
+ isBatchFinished,
80
+ isHeadOfQueue,
81
+ isQueueBusy,
82
+ isTerminalPhase,
83
+ manualCommand2 as manualCommand,
84
+ markBatchEntry,
85
+ needsRestartKeys,
86
+ nextBatchKey,
87
+ normalizeBatchSession,
88
+ normalizeQueueState,
89
+ orderTargets,
90
+ parseChangelog,
91
+ pruneExpiredIntents,
92
+ queuePositionOf,
93
+ releaseOwnerInQueue,
94
+ renderChangelogHTML,
95
+ resumeBatchSession,
96
+ selectChangelogEntries,
97
+ setOwnerIfFree,
98
+ visibleQueueFor
25
99
  };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * src/commands.ts — 安装命令的纯拼接(政策收归核心)。
3
+ *
4
+ * 由 update-core/src/commands.ts 原样拎入,两条产物形状冻结(改键改模板即破冰):
5
+ * 1. 执行配方(installRecipe)——真正安装时用:按宿主种类选路由,程序与参数数组分开,
6
+ * 使用范围名原样进数组;一律精确版本与官方源——第三方 Desktop 与普通宿主那两条把源写进
7
+ * 参数数组(带 --save-exact),官方桌面版那条只交一个精确版本规格,源走宿主管理器的选项。
8
+ * 2. 手工兜底文案(manualCommand)——给人复制到终端执行:沿用命令串形状,
9
+ * 使用范围名含特殊字符时加引号(只影响展示,不影响配方)。
10
+ * 自包含:不引用同层其它文件,版本号小工具按需内联。
11
+ * 类型引用只用 import type,转译后无运行时导入。
12
+ *
13
+ * 相对原样的增量(规格 #591 第 4、5 条):目标包名、官方源、安装时限经可选字段注入,
14
+ * 默认值等于现状常量。不传即走现状,老调用零变化。
15
+ */
16
+ import type { BlockedReason, EnvironmentKind, InstallRecipe, ReleaseChannel } from './ports.js';
17
+ /** 安装时限:15 分钟(起进程到退出,超时终止整棵进程树并按失败处理)。 */
18
+ export declare const INSTALL_TIMEOUT_MS: number;
19
+ /**
20
+ * 安装执行配方:宿主种类决定路由,使用范围决定目标。
21
+ *
22
+ * - 第三方 Desktop(desktop)→ desktop-service:由桌面端公开的 desktopPnpm 服务用参数数组
23
+ * 拉起打包好的 CLI,插件不碰 .cmd 垫片、不经 shell。
24
+ * - 官方桌面版(desktop-manager)→ desktop-manager:交给宿主进程内的插件管理器,参数只有
25
+ * 「add + 精确版本规格」——它只收一个 spec 字符串、不接受任何开关,源走它的 registry 选项。
26
+ * - 普通 DSH 宿主(cli)→ cli-process:用「当前运行时的可执行文件 + CLI 的 JS 入口
27
+ * + 参数数组」自己起进程;三系统同一套形态,差异只在可执行文件与入口由适配器提供。
28
+ *
29
+ * 前两条一律带官方源与 --save-exact;官方源不可达时由适配器诚实失败,不换源。
30
+ * 宿主种类不是已知三种取值、或目标包名不合管理器规格时不给配方(诚实失败转手工命令),不猜。
31
+ */
32
+ export declare function installRecipe(input: {
33
+ profileName: string | null;
34
+ version: string;
35
+ environmentKind: EnvironmentKind;
36
+ targetPackageName?: string;
37
+ registryUrl?: string;
38
+ timeoutMs?: number;
39
+ /** 版本通道(#16):默认 stable;显式传 prerelease 才可装预发布精确版。 */
40
+ releaseChannel?: ReleaseChannel;
41
+ }): InstallRecipe | null;
42
+ /**
43
+ * 手工兜底命令:给人复制到终端执行,形状沿用 `dsh plugin --profile <名> add <包>@<版本>`。
44
+ * 与配方同一套政策(精确版本、官方源、--save-exact),只是使用范围名含特殊字符时加引号,
45
+ * 因为这一串要经用户自己的 shell 解释。网络受限时用户可自行去掉 --registry=。
46
+ * 源码安装等不安全情形不给命令。
47
+ */
48
+ export declare function manualCommand(input: {
49
+ profileName: string | null;
50
+ latestVersion: string | null;
51
+ installedVersion: string | null;
52
+ runningVersion: string;
53
+ jobTargetVersion: string | null;
54
+ blockedReason: BlockedReason | null;
55
+ sourceInstall: boolean;
56
+ targetPackageName?: string;
57
+ registryUrl?: string;
58
+ releaseChannel?: ReleaseChannel;
59
+ }): string | null;
package/dist/commands.js CHANGED
@@ -6,6 +6,27 @@ const INSTALL_TIMEOUT_MS = 15 * 6e4;
6
6
  function validVersion(v) {
7
7
  return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v);
8
8
  }
9
+ function validPrereleaseIds(ids) {
10
+ if (typeof ids !== "string" || !ids) return false;
11
+ const parts = ids.split(".");
12
+ if (parts.length === 0) return false;
13
+ for (const p of parts) {
14
+ if (!p || !/^[0-9A-Za-z-]+$/.test(p)) return false;
15
+ if (/^\d+$/.test(p) && p.length > 1 && p.startsWith("0")) return false;
16
+ }
17
+ return true;
18
+ }
19
+ function validReleaseVersion(v) {
20
+ if (typeof v !== "string" || !v) return false;
21
+ const dash = v.indexOf("-");
22
+ if (dash < 0) return validVersion(v);
23
+ if (v.slice(dash + 1).includes("+")) return false;
24
+ return validVersion(v.slice(0, dash)) && validPrereleaseIds(v.slice(dash + 1));
25
+ }
26
+ function versionAllowed(version, channel) {
27
+ if (channel === "prerelease") return validReleaseVersion(version);
28
+ return validVersion(version);
29
+ }
9
30
  function parseTriple(v) {
10
31
  const parts = String(v).split(".");
11
32
  if (parts.length > 3) return null;
@@ -29,6 +50,39 @@ function compareVersions(a, b) {
29
50
  }
30
51
  return 0;
31
52
  }
53
+ function compareReleaseVersions(a, b) {
54
+ if (!validReleaseVersion(a) || !validReleaseVersion(b)) throw new Error("invalid-release");
55
+ const dashA = a.indexOf("-");
56
+ const dashB = b.indexOf("-");
57
+ const coreA = dashA < 0 ? a : a.slice(0, dashA);
58
+ const coreB = dashB < 0 ? b : b.slice(0, dashB);
59
+ const order = compareVersions(coreA, coreB);
60
+ if (order !== 0) return order;
61
+ const preA = dashA < 0 ? null : a.slice(dashA + 1).split(".");
62
+ const preB = dashB < 0 ? null : b.slice(dashB + 1).split(".");
63
+ if (preA === null && preB === null) return 0;
64
+ if (preA === null) return 1;
65
+ if (preB === null) return -1;
66
+ const width = Math.max(preA.length, preB.length);
67
+ for (let i = 0; i < width; i++) {
68
+ const x = preA[i];
69
+ const y = preB[i];
70
+ if (x === void 0) return -1;
71
+ if (y === void 0) return 1;
72
+ const xn = /^\d+$/.test(x) ? Number(x) : null;
73
+ const yn = /^\d+$/.test(y) ? Number(y) : null;
74
+ if (xn !== null && yn !== null) {
75
+ if (xn < yn) return -1;
76
+ if (xn > yn) return 1;
77
+ continue;
78
+ }
79
+ if (xn !== null) return -1;
80
+ if (yn !== null) return 1;
81
+ if (x < y) return -1;
82
+ if (x > y) return 1;
83
+ }
84
+ return 0;
85
+ }
32
86
  function usableProfileName(raw) {
33
87
  const name = typeof raw === "string" ? raw.trim() : "";
34
88
  if (!name || name.length > 255 || name.startsWith("-")) return null;
@@ -38,7 +92,8 @@ function usableProfileName(raw) {
38
92
  function installRecipe(input) {
39
93
  const name = usableProfileName(input?.profileName);
40
94
  const version = input?.version;
41
- if (!name || !validVersion(version)) return null;
95
+ const channel = input?.releaseChannel === "prerelease" ? "prerelease" : "stable";
96
+ if (!name || !versionAllowed(version, channel)) return null;
42
97
  const kind = input?.environmentKind;
43
98
  if (kind !== "desktop" && kind !== "desktop-manager" && kind !== "cli") return null;
44
99
  const targetName = input?.targetPackageName ?? PACKAGE_NAME;
@@ -71,14 +126,15 @@ function manualCommand(input) {
71
126
  const targetName = input.targetPackageName ?? PACKAGE_NAME;
72
127
  const registry = input.registryUrl ?? NPM_REGISTRY;
73
128
  if (!targetName || !registry) return null;
129
+ const channel = input.releaseChannel === "prerelease" ? "prerelease" : "stable";
74
130
  const arg = /^[A-Za-z0-9_.-]+$/.test(name) ? name : JSON.stringify(name);
75
- const picks = [input.latestVersion, input.jobTargetVersion, input.installedVersion].filter(validVersion);
131
+ const picks = [input.latestVersion, input.jobTargetVersion, input.installedVersion].filter((v) => versionAllowed(v, channel));
76
132
  let version = picks.length > 0 ? picks[0] : "latest";
77
133
  try {
78
- const ranked = picks.filter((v) => compareVersions(v, input.runningVersion) >= 0);
134
+ const ranked = picks.filter((v) => compareReleaseVersions(v, input.runningVersion) >= 0);
79
135
  if (ranked.length > 0) {
80
136
  version = ranked[0];
81
- for (const v of ranked) if (compareVersions(v, version) === 1) version = v;
137
+ for (const v of ranked) if (compareReleaseVersions(v, version) === 1) version = v;
82
138
  }
83
139
  } catch {
84
140
  }
@@ -0,0 +1,48 @@
1
+ export declare const DEFAULT_PREFIX = "wf";
2
+ export declare const PHONE_ACTIONS: readonly ["updateStatus", "updateCheck", "updateInstall"];
3
+ export type PhoneAction = (typeof PHONE_ACTIONS)[number];
4
+ export declare const DEFAULT_TARGET_PACKAGE = "dsh-mattpocock-skills-deck";
5
+ export declare const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
6
+ export declare const LEGACY_PLUGIN_ID = "dsh-mattpocock-skills-deck";
7
+ export declare const LEGACY_DIR_SEGMENT = "updates";
8
+ export declare const STATE_FILE = "state.json";
9
+ export declare const LOCK_FILE = "install.lock";
10
+ export declare const BACKUP_FILE = "before.json";
11
+ export declare const SKIPPED_FILE = "skipped.json";
12
+ export declare const RELEASE_CHANNELS: readonly ["stable", "prerelease"];
13
+ export type ReleaseChannelOption = (typeof RELEASE_CHANNELS)[number];
14
+ export declare const DEFAULT_CHECK_TIMEOUT_MS = 10000;
15
+ export declare const DEFAULT_CONFIRMATION_TTL_MS: number;
16
+ export declare const DEFAULT_INSTALL_TIMEOUT_MS: number;
17
+ export declare const DEFAULT_PANEL_POLL_MS = 1000;
18
+ export declare const RECHECK_WINDOW_MS = 2000;
19
+ export declare const MIN_PANEL_POLL_MS = 250;
20
+ export interface UpdateConfigInput {
21
+ pluginId: string;
22
+ prefix?: string;
23
+ targetPackageName?: string;
24
+ registryUrl?: string;
25
+ homeDir?: string;
26
+ checkTimeoutMs?: number;
27
+ confirmationTtlMs?: number;
28
+ installTimeoutMs?: number;
29
+ panelPollMs?: number;
30
+ releaseChannel?: ReleaseChannelOption;
31
+ }
32
+ export interface ResolvedUpdateConfig {
33
+ pluginId: string;
34
+ prefix: string;
35
+ targetPackageName: string;
36
+ registryUrl: string;
37
+ homeDir: string | null;
38
+ checkTimeoutMs: number;
39
+ confirmationTtlMs: number;
40
+ installTimeoutMs: number;
41
+ panelPollMs: number;
42
+ releaseChannel: ReleaseChannelOption;
43
+ }
44
+ export declare function assertPluginId(value: unknown): string;
45
+ export declare function assertPrefix(value: unknown, role: string): string;
46
+ export declare function resolveUpdateConfig(input: UpdateConfigInput): ResolvedUpdateConfig;
47
+ export declare function buildPhoneNames(prefix: string): Record<PhoneAction, string>;
48
+ export declare function buildPhoneName(prefix: string, action: PhoneAction): string;
package/dist/config.js CHANGED
@@ -8,6 +8,8 @@ const LEGACY_DIR_SEGMENT = "updates";
8
8
  const STATE_FILE = "state.json";
9
9
  const LOCK_FILE = "install.lock";
10
10
  const BACKUP_FILE = "before.json";
11
+ const SKIPPED_FILE = "skipped.json";
12
+ const RELEASE_CHANNELS = ["stable", "prerelease"];
11
13
  const DEFAULT_CHECK_TIMEOUT_MS = 1e4;
12
14
  const DEFAULT_CONFIRMATION_TTL_MS = 10 * 6e4;
13
15
  const DEFAULT_INSTALL_TIMEOUT_MS = 15 * 6e4;
@@ -60,7 +62,11 @@ function resolveUpdateConfig(input) {
60
62
  if (panelPollMs < MIN_PANEL_POLL_MS) {
61
63
  throw new Error("[dsh-plugin-update] \u9762\u677F\u8F6E\u8BE2 panelPollMs \u975E\u6CD5\uFF1A\u4E0D\u5F97\u5C0F\u4E8E 250 \u6BEB\u79D2\uFF08\u6536\u5230 " + JSON.stringify(input.panelPollMs) + "\uFF09");
62
64
  }
63
- return { pluginId, prefix, targetPackageName, registryUrl, homeDir, checkTimeoutMs, confirmationTtlMs, installTimeoutMs, panelPollMs };
65
+ const releaseChannel = input.releaseChannel === void 0 ? "stable" : input.releaseChannel;
66
+ if (releaseChannel !== "stable" && releaseChannel !== "prerelease") {
67
+ throw new Error("[dsh-plugin-update] \u7248\u672C\u901A\u9053 releaseChannel \u975E\u6CD5\uFF1A\u53EA\u6536 stable \u6216 prerelease\uFF08\u6536\u5230 " + JSON.stringify(input.releaseChannel) + "\uFF09");
68
+ }
69
+ return { pluginId, prefix, targetPackageName, registryUrl, homeDir, checkTimeoutMs, confirmationTtlMs, installTimeoutMs, panelPollMs, releaseChannel };
64
70
  }
65
71
  function buildPhoneNames(prefix) {
66
72
  const checked = assertPrefix(prefix, "\u7535\u8BDD\u540D\u524D\u7F00 prefix");
@@ -88,6 +94,8 @@ export {
88
94
  MIN_PANEL_POLL_MS,
89
95
  PHONE_ACTIONS,
90
96
  RECHECK_WINDOW_MS,
97
+ RELEASE_CHANNELS,
98
+ SKIPPED_FILE,
91
99
  STATE_FILE,
92
100
  assertPluginId,
93
101
  assertPrefix,
package/dist/diag.d.ts ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * src/diag.ts —— 电话侧失败证据 diag 落地(#21,承接 #18 契约)。
3
+ *
4
+ * 失败回包加可选内联小证据对象 `diag`:平时面板仍只按稳定码分支与文案映射,
5
+ * 复制诊断按钮用 `diag` 组出一段自包含、可粘贴、已脱敏的文本;日志通道保留用于深挖。
6
+ * `diag` 只增不改、旧面板宽容读(未知键与错类型一律忽略),缺省即省略;
7
+ * 脱敏是构造属性,无安全详情时宁可省略也不给原文。所有失败经一次回包即证据闭包,
8
+ * 不做二次往返;深证据超限再另议,不在本模块内。
9
+ *
10
+ * 16 键目录(第一版,线键永不改名):
11
+ * v / stage / route / method / httpStatus / exitCode / latencyMs / detail /
12
+ * targetPackageName / runningVersion / latestVersion / environmentKind /
13
+ * requestId / checkId / registryHost / action
14
+ * 其余一律易变(面板必须忽略),queuePos 等上游语义落定后再转正——本模块永不产出
15
+ * queuePos(保留未来键,见 STAGE_EXPECT 之外的显式断言)。
16
+ *
17
+ * 阶段枚举冻结并与真实接缝对齐(6 值):
18
+ * read-installed(读已装)/ fetch-release(取远端)/ validate-release(校验发行物)/
19
+ * preflight(装前复核)/ exec(执行)/ verify(装后校验)
20
+ * 路由与方法正交,不拼串。动作提示由包内政策按(阶段×信号)推导
21
+ * (retry / manual / contact / restart),面板只渲染不推导。
22
+ *
23
+ * 缺省即省略(无此格即本阶段不适用或未知),按(阶段×键)期望表断言;
24
+ * 对象版本只管大门(v=1),新增可选键不 bump 版本,面板永不分支于版本。
25
+ * 宽容读契约:未知键忽略、错类型忽略、缺省当正常;面板分支只允许用稳定码。
26
+ *
27
+ * 序列化墙:整个 diag JSON ≤ DIAG_INLINE_BUDGET_BYTES(1024,单源见 redaction.ts)。
28
+ * 按 #24 enforcement 按序丢弃(registryHost → action → 版本 → 其余可选,detail 只截断;
29
+ * action 可省,v/码/阶段/路由/请求编号永不丢)。verify 阶段暂无电话来源(装后校验走后台异步,
30
+ * 不经电话失败回包),枚举保留、电话侧不产出;running/latest 仅已知时带,未知即省略。
31
+ * 构造全程 best-effort:任何一步异常即回 undefined(调用方按无 diag 的旧形状返回,
32
+ * 新面板×旧载荷仍为正常缺省,不抛)。
33
+ */
34
+ /** diag 对象版本:只管大门,新增可选键不 bump,面板永不分支于版本。 */
35
+ export declare const DIAG_VERSION = 1;
36
+ /** 阶段枚举(冻结 6 值,与真实接缝对齐)。 */
37
+ export declare const DIAG_STAGES: readonly ["read-installed", "fetch-release", "validate-release", "preflight", "exec", "verify"];
38
+ export type DiagStage = (typeof DIAG_STAGES)[number];
39
+ /** 动作提示(冻结 4 值,包内按阶段×信号推导,面板只渲染)。 */
40
+ export declare const DIAG_ACTIONS: readonly ["retry", "manual", "contact", "restart"];
41
+ export type DiagAction = (typeof DIAG_ACTIONS)[number];
42
+ /** 16 键目录与线类型(面板宽容读的唯一依据)。 */
43
+ export declare const DIAG_KNOWN_TYPES: Record<string, 'number' | 'string'>;
44
+ export declare const DIAG_KEYS: string[];
45
+ /** (阶段×键)期望表:期望有 / 期望没有(缺省即省略,异常仅提示不改分支)。 */
46
+ export declare const DIAG_STAGE_EXPECT: Record<DiagStage, {
47
+ want: string[];
48
+ forbid: string[];
49
+ }>;
50
+ export interface DiagInput {
51
+ /** 稳定码(已收敛后的 error 格,如 check-failed / invalid-release / install-failed …)。 */
52
+ errorCode: string;
53
+ /** 电话种类:update-status / update-check / update-install(loggedPhone 的 kind)。 */
54
+ phoneKind: string;
55
+ /** 原始错误对象(读 httpStatus / exitCode / detail,不读栈)。 */
56
+ error?: unknown;
57
+ /** 调用入参(读 requestId / checkId,脏类型一律忽略)。 */
58
+ args?: Record<string, unknown>;
59
+ /** 包配置:目标包名与官方源(源主机名只给认可源)。 */
60
+ targetPackageName?: unknown;
61
+ registryUrl?: unknown;
62
+ /** 已知运行版本(显式覆盖或清单解析值,非版本形状一律省略)。 */
63
+ runningVersion?: unknown;
64
+ /** 已知远端版本(失败路径多半未知,未知即省略)。 */
65
+ latestVersion?: unknown;
66
+ /** 宿主种类(显式覆盖值,三取值之外一律省略)。 */
67
+ environmentKind?: unknown;
68
+ /** 本次电话耗时毫秒(只收有限非负数)。 */
69
+ latencyMs?: unknown;
70
+ }
71
+ export interface DiagObject {
72
+ v: number;
73
+ stage: DiagStage;
74
+ /** 路由与方法正交;exec 在宿主种类未知时省略(不猜,缺省即省略)。 */
75
+ route?: string;
76
+ method?: string;
77
+ httpStatus?: number;
78
+ exitCode?: number;
79
+ latencyMs?: number;
80
+ detail?: string;
81
+ targetPackageName?: string;
82
+ runningVersion?: string;
83
+ latestVersion?: string;
84
+ environmentKind?: string;
85
+ requestId?: string;
86
+ checkId?: string;
87
+ registryHost?: string;
88
+ action?: DiagAction;
89
+ /** 显式截断位(#24):仅超墙动过手时为 true,否则省略以保持 16 键纯净。 */
90
+ truncated?: boolean;
91
+ }
92
+ /**
93
+ * 阶段推导(第一性原理:阶段 = 哪道接缝挂了,由电话 × 码共同定位,不单看码)。
94
+ * - update-status 只读本地:一切失败归 read-installed(从不执行安装,install-failed 在此亦为读失败)。
95
+ * - update-check 两段:取数失败归 fetch-release,校验失败归 validate-release,其余环境失败归 read-installed。
96
+ * - update-install 三段:安装执行失败归 exec,取数重验失败归 fetch/validate,其余(含凭证/队列/环境)归 preflight。
97
+ * verify 暂无电话来源(装后校验走后台异步,不经电话失败回包),此处永不返回 verify。
98
+ */
99
+ export declare function deriveStage(errorCode: string, phoneKind: string): DiagStage;
100
+ /**
101
+ * 路由与方法正交(不拼串,各自独立断言)。
102
+ * - 取数/校验:registry/https;读已装/装后校验:disk/fs。
103
+ * - preflight:仅 update-busy 走 queue/fs(队列即公平门),其余(凭证/环境)走 disk/fs。
104
+ * - exec:路由即真实安装路由(由宿主种类定),方法为传输形态;宿主种类未知时省略(不猜)。
105
+ */
106
+ export declare function deriveRouteMethod(stage: DiagStage, environmentKind: unknown, errorCode?: string): {
107
+ route?: string;
108
+ method?: string;
109
+ };
110
+ /** 动作提示包内推导(面板只渲染,不自写状态码分支)。 */
111
+ export declare function deriveAction(stage: DiagStage, errorCode: string): DiagAction;
112
+ /**
113
+ * 组装 diag(best-effort:任何异常回 undefined,调用方按无 diag 的旧形状返回)。
114
+ * 永不产出 queuePos;类型恒对;缺省即省略。
115
+ */
116
+ export declare function buildDiag(input: DiagInput): DiagObject | undefined;
117
+ /**
118
+ * 序列化 ≤1024 字节墙(#24 单源):经 redaction.enforceDiagBudget 按序丢弃。
119
+ * truncated 位显式返回:false 即 16 键原样(删掉该位,保持 #18 目录纯净);
120
+ * true 即超墙动过手(保留该位,面板宽容读忽略它,旧面板剥掉 diag 仍逐字不变)。
121
+ * 仍超(理论上不可能,永不丢键兜底)则回 undefined 按无 diag 处理。
122
+ */
123
+ export declare function enforceBudget(diag: DiagObject): (DiagObject & {
124
+ truncated?: boolean;
125
+ }) | undefined;