@perrylink/dsh-plugin-doctor 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -63,5 +63,11 @@ SURVEY.md 全渠道检测方法梳理 + 判据出处
63
63
 
64
64
  ## 状态
65
65
 
66
- WIP 工作夹(非 git 仓,同 `dsh-ticktick-handoff` 口径)。检测器本体零依赖、可独立运行;
67
- 后续如需发布,可参照工作区发布流程另立仓库。
66
+ 正式仓库:GitHub `PerryLink/dsh-plugin-doctor`(MIT Apache-2.0),npm `@perrylink/dsh-plugin-doctor`
67
+ (latest=0.1.3,2026-09-07)。CI 用法:
68
+
69
+ ```powershell
70
+ npx --yes @perrylink/dsh-plugin-doctor@0.1.3 --repo . --no-smoke --only "静态·包结构,静态·cordis 契约扫描"
71
+ ```
72
+
73
+ 35 个插件仓已内置 `.github/workflows/plugin-doctor.yml`(install→build→npx 静态 R/K 门禁)。
@@ -65,6 +65,8 @@ export function addChecks(doctor, ctx) {
65
65
  }
66
66
  const candidates = new Map()
67
67
  for (const { f, text } of all) {
68
+ // 局部类型参数(如 createState(ctx: { credentials: ... }),非 cordis Context)——跳过该文件
69
+ if (/ctx\s*:\s*\{/.test(text)) continue
68
70
  for (const m of text.matchAll(/ctx\.([A-Za-z_$][\w$]*)/g)) {
69
71
  const name = m[1]
70
72
  if (CTX_INTRINSIC.has(name) || injectList.has(name)) continue
@@ -100,8 +102,20 @@ export function addChecks(doctor, ctx) {
100
102
  for (let i = 0; i < lines.length; i++) {
101
103
  const line = lines[i]
102
104
  if (!/(setInterval|setTimeout|setImmediate|process\.on|addEventListener\()/.test(line)) continue
105
+ // 变量级清理检测:同文件存在 clearTimeout/clearInterval/unref/removeEventListener 对应清理 → 自清理,跳过
106
+ // (变量声明可能在上一行,如三元表达式换行的 `const timer = cond\n ? setInterval(...)`)
107
+ let varMatch = line.match(/(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=/)
108
+ if (!varMatch && i > 0) varMatch = lines[i - 1].match(/(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=/)
109
+ const listenerCleaned = line.includes('addEventListener') && /removeEventListener\(/.test(text)
110
+ if (varMatch) {
111
+ const name = varMatch[1]
112
+ const timerCleaned = new RegExp(`clear(?:Timeout|Interval)\\(\\s*${name}\\b|${name}\\??\\.\\s*unref\\s*\\(`).test(text)
113
+ if (timerCleaned || listenerCleaned) continue
114
+ } else if (listenerCleaned) {
115
+ continue
116
+ }
103
117
  const before = lines.slice(Math.max(0, i - 3), i).join('\n')
104
- if (!/ctx\.effect|ctx\.scope|ctx\.on\b/.test(before) && !/return\s*\(\)\s*=>/.test(before + line)) {
118
+ if (!/ctx\.effect|ctx\.on\b/.test(before) && !/return\s*\(\)\s*=>/.test(before + line)) {
105
119
  hits.push(`${short(f)}:${i + 1} ${line.trim()}`)
106
120
  }
107
121
  }
@@ -67,11 +67,13 @@ export function addChecks(doctor, ctx) {
67
67
  const p = path.resolve(repoPath, patch)
68
68
  if (!existsSync(p)) return fail(`patch 文件不存在: ${patch}`)
69
69
  const text = readFileSync(p, 'utf8')
70
+ // YAML 注释行剔除后再做启发式(示例配置注释里常出现 name: xxx)
71
+ const active = text.split(/\r?\n/).filter((l) => !/^\s*#/.test(l)).join('\n')
70
72
  const problems = []
71
- if (!/-\s+insert\s*:/.test(text)) problems.push('未找到 "- insert:" 结构')
72
- const ids = [...text.matchAll(/(?:^|\n)\s*-?\s*id:\s*["']?([^"'\s]+)/g)].map((m) => m[1])
73
+ if (!/-\s+insert\s*:/.test(active)) problems.push('未找到 "- insert:" 结构')
74
+ const ids = [...active.matchAll(/(?:^|\n)\s*-?\s*id:\s*["']?([^"'\s]+)/g)].map((m) => m[1])
73
75
  if (!ids.length) problems.push('未找到 id 行(行 id 必须存在,供上层整行替换定位)')
74
- const names = [...text.matchAll(/(?:^|\n)\s*name:\s*["']?([^"'\s,]+)|[,\s]name:\s*["']?([^"'\s,]+)/g)]
76
+ const names = [...active.matchAll(/(?:^|\n)\s*name:\s*["']?([^"'\s,]+)|[,\s]name:\s*["']?([^"'\s,]+)/g)]
75
77
  .map((m) => m[1] ?? m[2]).filter(Boolean)
76
78
  const badNames = names.filter((n) => n !== pkgName && !n.startsWith(`${pkgName}/`))
77
79
  if (names.length && badNames.length) problems.push(`行 name 与包名不一致: ${[...new Set(badNames)].join(', ')}(name 必须经 profile node_modules 解析,应为包名)`)
@@ -121,7 +123,7 @@ export function addChecks(doctor, ctx) {
121
123
  const importsCordis = srcFiles.some((f) => { try { return /from\s+['"]@deepseek-ai\/cordis['"]|require\(['"]@deepseek-ai\/cordis['"]\)/.test(readFileSync(f, 'utf8')) } catch { return false } })
122
124
  if (!peer && !dep) {
123
125
  if (importsCordis) problems.push('源码 import @deepseek-ai/cordis 但未声明任何依赖')
124
- else problems.push('未声明 @deepseek-ai/cordis(若源码不 import 可忽略)')
126
+ else return pass('未声明 @deepseek-ai/cordis 且源码无 import(纯 JS 仓可无 cordis 运行时依赖)')
125
127
  } else if (!peer) {
126
128
  problems.push('@deepseek-ai/cordis 只在 dependencies(官方口径:peerDependencies + devDependencies 同时声明)')
127
129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@perrylink/dsh-plugin-doctor",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Zero-dependency static + sandbox smoke detector for DeepSeek Harness (dsh) plugins: package-structure gates (R), cordis contract scans (K), keyless-headless sandbox smoke (D), and ecosystem-listing checks (CC).",
5
5
  "type": "module",
6
6
  "main": "doctor.mjs",