@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.
- package/dsh-doctor.mjs +60 -38
- 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
|
-
/**
|
|
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+)
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
2076
|
-
*
|
|
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'
|
|
2082
|
-
if (raw === '*' || raw
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
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
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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.
|
|
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": [
|