@moonquake2004/dsh-doctor 0.4.24 → 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 +60 -38
  2. package/package.json +1 -1
package/dsh-doctor.mjs CHANGED
@@ -2025,13 +2025,31 @@ const sessionArg = (() => { const i = process.argv.indexOf('--session'); return
2025
2025
  * strict semver 的字面规则会把第一种判成不满足 —— 那是误报,而误报健康 profile 比不报更糟。
2026
2026
  */
2027
2027
 
2028
- /** 解析版本为 {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。 */
2029
2044
  function parseVer(v) {
2030
- 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 ?? ''));
2031
2046
  if (!m) return null;
2032
- 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
+ };
2033
2051
  }
2034
- /** semver 预发布优先级:数字段比数字,字母段按字典序,数字 < 字母;无 prerelease 者更高。 */
2052
+ /** semver 预发布优先级:数字段比数字,字母段按字典序,数字段 < 字母段;无 prerelease 者更高。 */
2035
2053
  function cmpVer(a, b) {
2036
2054
  for (let i = 0; i < 3; i++) if (a.nums[i] !== b.nums[i]) return a.nums[i] < b.nums[i] ? -1 : 1;
2037
2055
  if (!a.pre.length && !b.pre.length) return 0;
@@ -2048,8 +2066,8 @@ function cmpVer(a, b) {
2048
2066
  }
2049
2067
  return 0;
2050
2068
  }
2051
- /** 单个比较器是否满足(数值判定,已含预发布比较)。 */
2052
- function cmpSatisfied(ver, op, target) {
2069
+ /** 单个比较器:true / false / null(前缀无法解析)。 */
2070
+ function cmpHalf(ver, op, target) {
2053
2071
  const c = cmpVer(ver, target);
2054
2072
  switch (op) {
2055
2073
  case '>': return c > 0;
@@ -2057,10 +2075,9 @@ function cmpSatisfied(ver, op, target) {
2057
2075
  case '<': return c < 0;
2058
2076
  case '<=': return c <= 0;
2059
2077
  case '=': return c === 0;
2060
- default: return false;
2078
+ default: return null;
2061
2079
  }
2062
2080
  }
2063
- /** 展开 ^ / ~ 为区间对(只覆盖生态里实际出现的形态)。 */
2064
2081
  function expandCaret(target) {
2065
2082
  const [maj, min, pat] = target.nums;
2066
2083
  if (maj > 0) return { lower: ['>=', target], upper: ['<', parseVer(`${maj + 1}.0.0`)] };
@@ -2071,42 +2088,47 @@ function expandTilde(target) {
2071
2088
  const [maj, min] = target.nums;
2072
2089
  return { lower: ['>=', target], upper: ['<', parseVer(`${maj}.${min + 1}.0`)] };
2073
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
+ }
2074
2114
  /**
2075
- * 判定 range 是否接受 version。返回 { state: 'satisfied' | 'unsatisfied' | 'unknown', hasPreComparator }
2076
- * 未知的三种来源:`*`/空、区间不可解析、**纯 release 区间面对预发布安装版本**(不猜)。
2115
+ * 判定 range 是否接受 version。返回 { state: 'satisfied' | 'unsatisfied' | 'unknown' }。
2116
+ * 未知(null)的三种来源:`*`/空、比较器不可解析、预发布版本面对"非预发布感知"的组。
2077
2117
  */
2078
2118
  function peerRangeState(version, range) {
2079
2119
  const ver = parseVer(version);
2080
2120
  const raw = String(range ?? '').trim();
2081
- if (!ver || !raw) return { state: 'unknown', hasPreComparator: null };
2082
- if (raw === '*' || raw === '' || raw === 'x' || raw === 'latest') return { state: 'unknown', hasPreComparator: false };
2083
- let hasPre = false;
2084
- const groups = raw.split('||').map((g) => g.trim()).filter(Boolean);
2085
- 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;
2086
2126
  for (const g of groups) {
2087
- const parts = g.split(/\s+/).filter(Boolean);
2088
- let ok = true; let preHere = false;
2089
- for (const part of parts) {
2090
- const m = /^([\^~]|>=|<=|>|<|=)?\s*v?(.+)$/.exec(part);
2091
- if (!m) { ok = false; break; }
2092
- const op = m[1] ?? '=';
2093
- const target = parseVer(m[2]);
2094
- if (!target) { ok = false; break; }
2095
- if (target.pre.length) preHere = true;
2096
- if (op === '^' || op === '~') {
2097
- const { lower, upper } = op === '^' ? expandCaret(target) : expandTilde(target);
2098
- if (!(cmpSatisfied(ver, lower[0], lower[1]) && cmpSatisfied(ver, upper[0], upper[1]))) { ok = false; break; }
2099
- } else if (!cmpSatisfied(ver, op, target)) { ok = false; break; }
2100
- }
2101
- if (ok) {
2102
- hasPre = hasPre || preHere;
2103
- return { state: 'satisfied', hasPreComparator: hasPre };
2104
- }
2105
- hasPre = hasPre || preHere;
2106
- }
2107
- // 全部组都不满足:若这次不匹配发生在"纯 release 区间 vs 预发布版本",按规则记 unknown
2108
- if (!hasPre && ver.pre.length) return { state: 'unknown', hasPreComparator: false };
2109
- 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' };
2110
2132
  }
2111
2133
 
2112
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.24",
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": [