@moonquake2004/dsh-doctor 0.4.23 → 0.4.25

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 (2) hide show
  1. package/dsh-doctor.mjs +67 -42
  2. package/package.json +1 -1
package/dsh-doctor.mjs CHANGED
@@ -241,8 +241,11 @@ function resolveProfile(name) {
241
241
 
242
242
  /* ================= env ================= */
243
243
  /** v1 词汇表 r5(#1719/#2259)node 语义:pass = 满足 ^22.19.0 || >=24.0.0,其余 warn——无民间 fail 阈值。
244
- * 注(2026-09 更正):该范围来自 v1 词汇表对齐,**不是**从某个 package.json 读来的;
245
- * 已发布的 `@deepseek-ai/dsh@0.1.5-rc.1` **不含 engines 字段**,此前文案写"root package.json engines"属出处误引。 */
244
+ * 出处(2026-09 两次更正后的准确版本,由社区 @ciceroyang 核实、我们复核):
245
+ * 该范围**确实声明在 manifest 里** —— 仓库根 `package.json` 的 `engines.node`,
246
+ * 即 `@deepseek-ai/dsh-root`(`private: true`,非发布包)。**没有任何已发布包继承它**,
247
+ * 所以 registry 查不到、npm 安装时既不校验也不警告;这正是社区里"没有 engines"印象的由来。
248
+ * 我们内置的范围与之一致(等于官方声明值),但它是**手写维护**的:上游改动时需同步。 */
246
249
  function nodeInSupportedRange(v) {
247
250
  const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(v));
248
251
  if (!m) return false;
@@ -266,8 +269,8 @@ function checkEnv() {
266
269
  const version = String(nv.stdout).trim();
267
270
  const supported = nodeInSupportedRange(version);
268
271
  report('env', 'E3-node', supported,
269
- supported ? `node ${version}(满足 v1 词汇表支持范围 ^22.19.0 || >=24.0.0;该版本未发布 engines 字段,#2259)` : `node ${version} 不在支持范围(^22.19.0 || >=24.0.0,v1 词汇表)——会话日志读取等能力受限`,
270
- supported ? undefined : '升级 node 到 ^22.19.0 或 >=24.0.0(v1 词汇表范围,见 #2259)');
272
+ supported ? `node ${version}(满足声明范围 ^22.19.0 || >=24.0.0——出处为仓库根私有 workspace package.json 的 engines,#2259)` : `node ${version} 不在支持范围(^22.19.0 || >=24.0.0,v1 词汇表)——会话日志读取等能力受限`,
273
+ supported ? undefined : '升级 node 到 ^22.19.0 或 >=24.0.0(仓库根 engines 声明的范围,见 #2259)');
271
274
  }
272
275
 
273
276
  // E12:运行时 zstd 稳定性(#6651 的运行时线索)
@@ -2022,13 +2025,31 @@ const sessionArg = (() => { const i = process.argv.indexOf('--session'); return
2022
2025
  * strict semver 的字面规则会把第一种判成不满足 —— 那是误报,而误报健康 profile 比不报更糟。
2023
2026
  */
2024
2027
 
2025
- /** 解析版本为 {nums:[a,b,c], pre:[...]};无法解析返回 null。 */
2028
+ /**
2029
+ * 版本与区间判定 —— **按参考实现的代码对齐**(ciceroyang/dsh-doctor `satisfiesRange`),
2030
+ * 并经其差分测试 `scripts/range-difftest.mjs` 验收(13 条边界语料 0 分歧)。
2031
+ *
2032
+ * 规则(照其代码,而非文档表格——两者不一致,见下):
2033
+ * 1. 逐组做**数值**判定;任一比较器为假 → 该组 false;有无法解析的比较器 → 该组 null;
2034
+ * 2. 全部比较器数值通过后,若**被判版本是预发布**且**该组不含任何预发布比较器** → 该组 null;
2035
+ * 否则 → 该组 true;
2036
+ * 3. 任一组成立 → 兼容;否则若有组为 null → 未知;否则 → 不兼容。
2037
+ *
2038
+ * 文档表格第二行("numeric 满足但 strict 不满足 → unknown")与其代码/后果清单不一致:
2039
+ * 代码对 `>=0.1.0-rc.5 <0.2.0` 配 `0.1.5-rc.2` 返回**兼容**(该组是预发布感知的),
2040
+ * 而按表格字面会得到"未知"。我们按**代码**对齐,并已把该不一致回报给作者。
2041
+ */
2042
+
2043
+ /** 解析(可含部分的)版本:1 / 1.2 / 1.2.3[-pre];缺失段补 0。无法解析返回 null。 */
2026
2044
  function parseVer(v) {
2027
- const m = /^\s*v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(String(v ?? ''));
2045
+ const m = /^\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?\s*$/.exec(String(v ?? ''));
2028
2046
  if (!m) return null;
2029
- return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ? m[4].split('.') : [] };
2047
+ return {
2048
+ nums: [Number(m[1]), m[2] === undefined ? 0 : Number(m[2]), m[3] === undefined ? 0 : Number(m[3])],
2049
+ pre: m[4] ? m[4].split('.') : [],
2050
+ };
2030
2051
  }
2031
- /** semver 预发布优先级:数字段比数字,字母段按字典序,数字 < 字母;无 prerelease 者更高。 */
2052
+ /** semver 预发布优先级:数字段比数字,字母段按字典序,数字段 < 字母段;无 prerelease 者更高。 */
2032
2053
  function cmpVer(a, b) {
2033
2054
  for (let i = 0; i < 3; i++) if (a.nums[i] !== b.nums[i]) return a.nums[i] < b.nums[i] ? -1 : 1;
2034
2055
  if (!a.pre.length && !b.pre.length) return 0;
@@ -2045,8 +2066,8 @@ function cmpVer(a, b) {
2045
2066
  }
2046
2067
  return 0;
2047
2068
  }
2048
- /** 单个比较器是否满足(数值判定,已含预发布比较)。 */
2049
- function cmpSatisfied(ver, op, target) {
2069
+ /** 单个比较器:true / false / null(前缀无法解析)。 */
2070
+ function cmpHalf(ver, op, target) {
2050
2071
  const c = cmpVer(ver, target);
2051
2072
  switch (op) {
2052
2073
  case '>': return c > 0;
@@ -2054,10 +2075,9 @@ function cmpSatisfied(ver, op, target) {
2054
2075
  case '<': return c < 0;
2055
2076
  case '<=': return c <= 0;
2056
2077
  case '=': return c === 0;
2057
- default: return false;
2078
+ default: return null;
2058
2079
  }
2059
2080
  }
2060
- /** 展开 ^ / ~ 为区间对(只覆盖生态里实际出现的形态)。 */
2061
2081
  function expandCaret(target) {
2062
2082
  const [maj, min, pat] = target.nums;
2063
2083
  if (maj > 0) return { lower: ['>=', target], upper: ['<', parseVer(`${maj + 1}.0.0`)] };
@@ -2068,42 +2088,47 @@ function expandTilde(target) {
2068
2088
  const [maj, min] = target.nums;
2069
2089
  return { lower: ['>=', target], upper: ['<', parseVer(`${maj}.${min + 1}.0`)] };
2070
2090
  }
2091
+ /** 一个 `||` 组的判定:true / false / null。 */
2092
+ function evalGroup(ver, group) {
2093
+ let prereleaseAware = false;
2094
+ for (const part of group.split(/\s+/).filter(Boolean)) {
2095
+ const m = /^([\^~]|>=|<=|>|<|=)?\s*v?(.+)$/.exec(part);
2096
+ if (!m) return null;
2097
+ const op = m[1] ?? '=';
2098
+ const target = parseVer(m[2]);
2099
+ if (!target) return null;
2100
+ if (target.pre.length > 0) prereleaseAware = true;
2101
+ if (op === '^' || op === '~') {
2102
+ const { lower, upper } = op === '^' ? expandCaret(target) : expandTilde(target);
2103
+ if (cmpHalf(ver, lower[0], lower[1]) === false || cmpHalf(ver, upper[0], upper[1]) === false) return false;
2104
+ } else {
2105
+ const r = cmpHalf(ver, op, target);
2106
+ if (r === false) return false;
2107
+ if (r === null) return null;
2108
+ }
2109
+ }
2110
+ // 数值全通过:预发布版本只对"预发布感知"的组才敢断言兼容
2111
+ if (ver.pre.length > 0 && !prereleaseAware) return null;
2112
+ return true;
2113
+ }
2071
2114
  /**
2072
- * 判定 range 是否接受 version。返回 { state: 'satisfied' | 'unsatisfied' | 'unknown', hasPreComparator }
2073
- * 未知的三种来源:`*`/空、区间不可解析、**纯 release 区间面对预发布安装版本**(不猜)。
2115
+ * 判定 range 是否接受 version。返回 { state: 'satisfied' | 'unsatisfied' | 'unknown' }。
2116
+ * 未知(null)的三种来源:`*`/空、比较器不可解析、预发布版本面对"非预发布感知"的组。
2074
2117
  */
2075
2118
  function peerRangeState(version, range) {
2076
2119
  const ver = parseVer(version);
2077
2120
  const raw = String(range ?? '').trim();
2078
- if (!ver || !raw) return { state: 'unknown', hasPreComparator: null };
2079
- if (raw === '*' || raw === '' || raw === 'x' || raw === 'latest') return { state: 'unknown', hasPreComparator: false };
2080
- let hasPre = false;
2081
- const groups = raw.split('||').map((g) => g.trim()).filter(Boolean);
2082
- if (!groups.length) return { state: 'unknown', hasPreComparator: null };
2121
+ if (!ver || !raw) return { state: 'unknown' };
2122
+ if (raw === '*' || /^x$/i.test(raw) || raw.toLowerCase() === 'latest') return { state: 'unknown' };
2123
+ const groups = raw.split('||').map((x) => x.trim()).filter(Boolean);
2124
+ if (!groups.length) return { state: 'unknown' };
2125
+ let undecidable = false;
2083
2126
  for (const g of groups) {
2084
- const parts = g.split(/\s+/).filter(Boolean);
2085
- let ok = true; let preHere = false;
2086
- for (const part of parts) {
2087
- const m = /^([\^~]|>=|<=|>|<|=)?\s*v?(.+)$/.exec(part);
2088
- if (!m) { ok = false; break; }
2089
- const op = m[1] ?? '=';
2090
- const target = parseVer(m[2]);
2091
- if (!target) { ok = false; break; }
2092
- if (target.pre.length) preHere = true;
2093
- if (op === '^' || op === '~') {
2094
- const { lower, upper } = op === '^' ? expandCaret(target) : expandTilde(target);
2095
- if (!(cmpSatisfied(ver, lower[0], lower[1]) && cmpSatisfied(ver, upper[0], upper[1]))) { ok = false; break; }
2096
- } else if (!cmpSatisfied(ver, op, target)) { ok = false; break; }
2097
- }
2098
- if (ok) {
2099
- hasPre = hasPre || preHere;
2100
- return { state: 'satisfied', hasPreComparator: hasPre };
2101
- }
2102
- hasPre = hasPre || preHere;
2103
- }
2104
- // 全部组都不满足:若这次不匹配发生在"纯 release 区间 vs 预发布版本",按规则记 unknown
2105
- if (!hasPre && ver.pre.length) return { state: 'unknown', hasPreComparator: false };
2106
- return { state: 'unsatisfied', hasPreComparator: hasPre };
2127
+ const v = evalGroup(ver, g);
2128
+ if (v === true) return { state: 'satisfied' };
2129
+ if (v === null) undecidable = true;
2130
+ }
2131
+ return { state: undecidable ? 'unknown' : 'unsatisfied' };
2107
2132
  }
2108
2133
 
2109
2134
  /** 解析 host 包版本:profile node_modules → 共享镜像根;**跟随软链接**(readFileSync 会跟随)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.4.23",
3
+ "version": "0.4.25",
4
4
  "description": "Offline diagnostic for DeepSeek Harness — 28 built-in + 5 catalog checks across env/profile/session (Layer A checks-as-data), self-update (Layer B), and a semi-automatic LLM observer (Layer C, --observe); Doctor panel in web UI settings.",
5
5
  "main": "lib/index.js",
6
6
  "files": [