dsh-plugin-upgrade 0.1.3 → 2.0.1
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/AGENTS.md +146 -47
- package/CHANGELOG.md +106 -31
- package/README-es.md +241 -0
- package/README-hi.md +241 -0
- package/README-pt.md +241 -0
- package/README-zh.md +241 -0
- package/README.md +115 -46
- package/SECURITY.md +1 -1
- package/THIRD_PARTY_NOTICES.md +3 -3
- package/cordis.patch.yml +6 -6
- package/docs/EVIDENCE.md +423 -0
- package/index.mjs +21 -17
- package/lib/route.mjs +112 -0
- package/lib/scan-0.1.6.mjs +310 -0
- package/lib/scan.mjs +264 -31
- package/package.json +55 -10
- package/scripts/changelog-section.mjs +1 -0
- package/scripts/check-readme-sync.mjs +4 -1
- package/scripts/scan-0.1.5.mjs +20 -6
- package/scripts/sweep-all.mjs +6 -2
- package/scripts/verify-artifacts.mjs +44 -14
- package/scripts/verify-self-contained.mjs +13 -3
- package/skills/plugin-upgrade/SKILL.md +88 -0
- package/skills/plugin-upgrade/references/v0.1.3-alpha.1-to-v0.1.5-rc.1.md +421 -0
- package/skills/plugin-upgrade/references/v0.1.5-rc.2-to-v0.1.6-alpha.2.md +75 -0
- package/skills/plugin-upgrade/scripts/scan-0.1.5.mjs +29 -0
- package/types.d.ts +39 -7
- package/README.es.md +0 -172
- package/README.hi.md +0 -172
- package/README.pt.md +0 -172
- package/README.zh.md +0 -172
- package/skills/plugin-upgrade-015/SKILL.md +0 -38
- package/skills/plugin-upgrade-015/references/v0.1.3-alpha.1-to-v0.1.5-alpha.1.md +0 -90
- package/skills/plugin-upgrade-015/scripts/scan-0.1.5.mjs +0 -14
package/lib/scan.mjs
CHANGED
|
@@ -1,26 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
3
|
/**
|
|
3
|
-
* scan-0.1.5.mjs — zero-dependency detector for the DSH `0.1.3-alpha.1 →
|
|
4
|
-
* plugin-adaptation seams.
|
|
4
|
+
* scan-0.1.5.mjs — zero-dependency detector for the DSH `0.1.3-alpha.1 →
|
|
5
|
+
* 0.1.5-rc.1` plugin-adaptation seams.
|
|
5
6
|
*
|
|
6
|
-
* Why this exists:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
7
|
+
* Why this exists: this span's breakage is mostly *silent*. Two examples that
|
|
8
|
+
* motivate the whole catalog:
|
|
9
|
+
* - the bare `conversation` client slot was deleted with no alias, and
|
|
10
|
+
* `ctx.slots.inject()` only runs its callback when the declaration exists —
|
|
11
|
+
* so a client half that still targets it stops mounting without an error,
|
|
12
|
+
* a log line or a failed build (`C1`);
|
|
13
|
+
* - `assistant/message` gained a required `stream` field, so a log written
|
|
14
|
+
* without it imports fine and then refuses to resume (`S3`).
|
|
15
|
+
* A green local gate is therefore NOT evidence of adaptation: the published
|
|
16
|
+
* type line hides both deletions entirely.
|
|
17
|
+
*
|
|
18
|
+
* This scanner is the MERGED catalog for the whole `0.1.3-alpha.1 → 0.1.5-rc.1`
|
|
19
|
+
* corridor. It supersedes two version-locked predecessors that each covered one
|
|
20
|
+
* hop of the same span:
|
|
21
|
+
* - leg A `0.1.3-alpha.1 → 0.1.5-alpha.1` — seams `S1`–`S10`, `M1`
|
|
22
|
+
* (formerly `dsh-plugin-upgrade`)
|
|
23
|
+
* - leg B `0.1.5-alpha.1 → 0.1.5-rc.1` — seams `C1`–`C5`, `H1`–`H4`, `P1`
|
|
24
|
+
* (formerly `dsh-plugin-upgrade-rc1`)
|
|
25
|
+
* `M1` and the former leg-B `C3` were the same defect (a local gate compiling a
|
|
26
|
+
* stale type line), so they are one seam here; the card records the old spelling.
|
|
27
|
+
* The hop `0.1.5-rc.1 → 0.1.5-rc.2` added no plugin-facing seam, so the span
|
|
28
|
+
* ends at rc.1 by construction.
|
|
29
|
+
*
|
|
30
|
+
* The catalog below is the single source of truth shared by the version card,
|
|
31
|
+
* the packaged skill and this CLI. `test/card.test.mjs` fails when the card and
|
|
32
|
+
* this catalog disagree about the seam ids.
|
|
13
33
|
*
|
|
14
34
|
* Usage:
|
|
15
|
-
* node scan-0.1.5.mjs [--repo <path>] [--json <out.json>] [--seams S3,
|
|
35
|
+
* node scan-0.1.5.mjs [--repo <path>] [--json <out.json>] [--seams S3,C1] [--quiet]
|
|
16
36
|
*
|
|
17
37
|
* Exit codes: 0 = no error-severity hit, 1 = at least one error-severity hit,
|
|
18
38
|
* 2 = usage/scan failure.
|
|
19
39
|
*
|
|
20
|
-
* Provenance:
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
40
|
+
* Provenance: every upstream fact behind a seam was re-read from the harness
|
|
41
|
+
* checkout and recorded with `path:line` in `docs/EVIDENCE.md` and in the
|
|
42
|
+
* version card
|
|
43
|
+
* (`skills/plugin-upgrade/references/v0.1.3-alpha.1-to-v0.1.5-rc.1.md`).
|
|
44
|
+
* This scanner ships so a plugin author can re-measure their own repository; it
|
|
45
|
+
* imports nothing outside Node's standard library and never writes inside the
|
|
46
|
+
* scanned tree.
|
|
24
47
|
*/
|
|
25
48
|
|
|
26
49
|
import fs from 'node:fs'
|
|
@@ -29,10 +52,34 @@ import path from 'node:path'
|
|
|
29
52
|
const SKIP_DIRS = new Set(['node_modules', 'lib', 'dist', '.git', '.tmp', 'coverage', '_scratch', '_archive', 'downloads', 'upstream', 'dev'])
|
|
30
53
|
const SCAN_EXT = /\.(ts|tsx|mts|cts|mjs|cjs|js|jsx|json|yml|yaml)$/
|
|
31
54
|
|
|
32
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {object} Seam
|
|
57
|
+
* @property {string} id
|
|
58
|
+
* @property {string} title
|
|
59
|
+
* @property {'error'|'warn'|'info'} severity
|
|
60
|
+
* @property {string} action
|
|
61
|
+
* @property {RegExp|null} test null = structured or card-only, deliberately not detected line-wise
|
|
62
|
+
* @property {(line: string) => boolean} [lineFilter]
|
|
63
|
+
* @property {(lines: string[], i: number) => boolean} [windowFilter]
|
|
64
|
+
* @property {(text: string, file: string) => boolean} [fileCheck]
|
|
65
|
+
* @property {(text: string, file: string) => boolean} [fileCheck2]
|
|
66
|
+
* @property {(text: string) => boolean} [downgradeIf]
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @typedef {object} Hit
|
|
71
|
+
* @property {string} seam
|
|
72
|
+
* @property {string} severity
|
|
73
|
+
* @property {string} file
|
|
74
|
+
* @property {number} line
|
|
75
|
+
* @property {string} snippet
|
|
76
|
+
* @property {string} detail
|
|
77
|
+
*/
|
|
33
78
|
|
|
34
79
|
/** @type {Seam[]} */
|
|
35
80
|
export const SEAMS = [
|
|
81
|
+
// ---------------------------------------------------------------- leg A
|
|
82
|
+
// 0.1.3-alpha.1 → 0.1.5-alpha.1
|
|
36
83
|
{
|
|
37
84
|
id: 'S3',
|
|
38
85
|
title: 'assistant/message 缺 stream(V3 必填)',
|
|
@@ -73,10 +120,10 @@ export const SEAMS = [
|
|
|
73
120
|
},
|
|
74
121
|
{
|
|
75
122
|
id: 'M1',
|
|
76
|
-
title: '
|
|
123
|
+
title: '本地门禁编译的是过期类型线 → typecheck 静默回退(假绿)',
|
|
77
124
|
severity: 'error',
|
|
78
|
-
action: 'tsconfig `paths`
|
|
79
|
-
test:
|
|
125
|
+
action: '两种独立成因,任何一种都让本地门禁变成假绿:(a) dev/test 依赖钉在 `0.1.5-alpha.*`,本地类型看不到 rc.1 的删除;(b) tsconfig `paths` 指向不存在的 checkout 目录时 TypeScript 静默回退到 node_modules 的已发布类型。把 dev/test 依赖钉到当前 rc 线,并保证每条 checkout 别名都能解析。**本接缝在 leg B 卡片上曾记作 `C3`,是同一条缺陷。**',
|
|
126
|
+
test: null,
|
|
80
127
|
},
|
|
81
128
|
{
|
|
82
129
|
id: 'S4',
|
|
@@ -135,8 +182,99 @@ export const SEAMS = [
|
|
|
135
182
|
action: '宿主事件词表 fail-closed 且 `Session.append` 无 `ignorable` 写入通道;无条件 append 会让会话不可读,请保留"探测后降级"的写法。',
|
|
136
183
|
test: /SessionEventMap|\.append\(/,
|
|
137
184
|
},
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------- leg B
|
|
187
|
+
// 0.1.5-alpha.1 → 0.1.5-rc.1
|
|
188
|
+
{
|
|
189
|
+
id: 'C1',
|
|
190
|
+
title: '裸 slot `conversation` 已删除(无别名)→ UI 静默不挂载',
|
|
191
|
+
severity: 'error',
|
|
192
|
+
action: '把 `ctx.slots.inject(\'conversation\', …)` / `slots.register({ name: \'conversation\' })` 改写成 `main.conversation`(会话内容位)或 `main`(全局中央面板,需 `key`)。rc.1 没有别名,也没有 deprecation 说明:回调永不执行,插件 UI 消失且没有任何报错。',
|
|
193
|
+
test: /['"]conversation['"]/,
|
|
194
|
+
// Same-line and multi-line call forms both count; the key must sit in a
|
|
195
|
+
// slot-facing context, never in prose or an unrelated settings string.
|
|
196
|
+
windowFilter: (lines, i) => {
|
|
197
|
+
const win = lines.slice(Math.max(0, i - 2), i + 2).join('\n')
|
|
198
|
+
return /\.inject\s*\(/.test(win) || /register\s*\(\s*\{/.test(win) || /name\s*:\s*['"]conversation['"]/.test(win)
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'C2',
|
|
203
|
+
title: 'npm 包改名:`dsh-client-ui-sidebar-textpreview` → `…-sidebar-documentpreview`',
|
|
204
|
+
severity: 'error',
|
|
205
|
+
action: '把 peer / optional peer / import / lockfile 里的 `@deepseek-ai/dsh-client-ui-sidebar-textpreview` 改为 `@deepseek-ai/dsh-client-ui-sidebar-documentpreview`。旧名在 rc.1 的 `packages/client/` 下已不存在,也没有 shim 包;做文档预览的插件改注册到 `sidebar.right.tab.document`。',
|
|
206
|
+
test: /sidebar-textpreview/,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'P1',
|
|
210
|
+
title: 'peer 区间缺了第二段 → `0.1.5-rc.1` 被 semver 拒绝',
|
|
211
|
+
severity: 'error',
|
|
212
|
+
action: '`>=0.1.2-rc.1 <0.2.0` 单段在 semver 下**不满足** `0.1.5-rc.1`(实测 semver 7.8.5 → false):npm 的 prerelease-tuple 规则只在同一 `[major,minor,patch]` 元组上存在带 prerelease 的 comparator 时才放行。必须保留 `|| >=0.1.5-alpha.1 <0.2.0` 这一段,rc.1 适配**不改** peer 区间。',
|
|
213
|
+
test: null,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'C4',
|
|
217
|
+
title: 'rc.1 新增全局面板模型,且每个 slot 多一个 `usePanelInfo` 标准 prop',
|
|
218
|
+
severity: 'warn',
|
|
219
|
+
action: '纯增量:rc.1 给几乎每个 slot 的 standardProps 追加了 `usePanelInfo: UsePanelInfo`(47 个文件提及),并新增 `main`(keyed/root)、`sidebar.panellist`(list/root)、`ctx.layout.selectPanel(MainPanelId | null)`、`ctx.layout.beginNavigation()`。用官方 `ComposedProps` 的仓零改动;手写 props 接口的组件需要复核。',
|
|
220
|
+
test: /usePanelInfo|selectPanel\(|sidebar\.panellist|MainPanelId|beginNavigation\(/,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
id: 'C5',
|
|
224
|
+
title: '右侧文档预览迁到 keyed slot `sidebar.right.tab.document`',
|
|
225
|
+
severity: 'warn',
|
|
226
|
+
action: 'TextPreview 的行为迁到 `sidebar.right.tab.document`(keyed/session,ownerProps = `DocumentContent`)。`sidebar.right.pane.tab` / `.title` 仍然存在,但 `declaredBy` 从 `rightbar` 变成 `rightbar.session`:inject `rightbar` 取得 pane 的插件要复核注入目标。',
|
|
227
|
+
test: /sidebar\.right\.pane\.tab|TextPreview|DocumentContent/,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'H1',
|
|
231
|
+
title: 'fail-closed 会话事件词表新增两个类型',
|
|
232
|
+
severity: 'warn',
|
|
233
|
+
action: '`KNOWN_SESSION_EVENT_TYPES` 新增 `deliverables/presented` 与 `subagent/catalog`。枚举过该词表、或自建事件白名单 / 计数快照的读取方要重新快照;否则新事件从「未知跳过」变成进入 surface,计数与渲染都会变。',
|
|
234
|
+
test: /KNOWN_SESSION_EVENT_TYPES|deliverables\/presented|subagent\/catalog/,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'H2',
|
|
238
|
+
title: '新工具 `present` 占用了 tool view 的 key `\'present\'`',
|
|
239
|
+
severity: 'warn',
|
|
240
|
+
action: 'rc.1 的新工具 `present` 自带 PresentRow,注册在 `tool.call.toolview` 的 key `\'present\'`(`packages/client/ui-deliverables/src/client/index.ts:62`),并且 `present` 因此进入 `conversation.chat.node` 的 already-taken keyDomain(`grep` 与 `read` 之间)。alpha.1 时该 key 是空闲的:已占用它的插件会被官方行顶掉,请换 key。',
|
|
241
|
+
test: /['"]present['"]/,
|
|
242
|
+
lineFilter: line => /toolview|tool\.call|chat\.node|key\s*:\s*['"]present['"]/.test(line),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'H4',
|
|
246
|
+
title: 'DeepSeek 适配器的默认咨询模型目录改以 `deepseek-flash` 打头',
|
|
247
|
+
severity: 'info',
|
|
248
|
+
action: 'rc.1 起 `llm-deepseek` 的默认 `models` 目录第一项是 `deepseek-flash`(name `DeepSeek-V41-Flash`,commit `bc5fd3b8dc`),README 的 `models` 默认行同步改为「V41 Flash + V4 Flash + V4 Pro + V4 Flash Vision Exp」。硬编码模型 id、或假定目录首项即默认模型的插件复核。',
|
|
249
|
+
test: /['"]deepseek-(?:chat|reasoner|v[0-9][a-z0-9.-]*|flash[a-z0-9.-]*)['"]/,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: 'H3',
|
|
253
|
+
title: 'rc.1 新增可选能力(纯增量,卡片列出,不做自动检测)',
|
|
254
|
+
severity: 'info',
|
|
255
|
+
action: '`ctx.sessionFeedback`(`command-feedback`);`ctx.layout.selectPanel()` / `beginNavigation()`;`ctx.workspaces.openSession()` / `openWorkspace()` / `forkSession()`;新品牌类型 `MainPanelId`。全部是增量:不接入不会有任何破坏,接入是可选收益。本接缝刻意没有自动检测。',
|
|
256
|
+
test: null,
|
|
257
|
+
},
|
|
138
258
|
]
|
|
139
259
|
|
|
260
|
+
/** Seam ids in card order; the version card must name exactly this set. */
|
|
261
|
+
export const SEAM_IDS = SEAMS.map(s => s.id)
|
|
262
|
+
|
|
263
|
+
/** Seams implemented structurally (not by regex), like `M1` and `P1`. */
|
|
264
|
+
const STRUCTURED = new Set(['M1', 'P1'])
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Seams that are documented on the card and id-parity checked, but deliberately
|
|
268
|
+
* have no automatic detection (pure additive capabilities).
|
|
269
|
+
*/
|
|
270
|
+
export const CARD_ONLY = SEAMS.filter(s => s.test === null && !STRUCTURED.has(s.id)).map(s => s.id)
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Yield every scannable file under `dir`, depth-limited and read-only.
|
|
274
|
+
* @param {string} dir
|
|
275
|
+
* @param {number} [depth]
|
|
276
|
+
* @returns {Generator<string, void, void>}
|
|
277
|
+
*/
|
|
140
278
|
function* walk(dir, depth = 0) {
|
|
141
279
|
if (depth > 8) return
|
|
142
280
|
let ents
|
|
@@ -151,16 +289,67 @@ function* walk(dir, depth = 0) {
|
|
|
151
289
|
}
|
|
152
290
|
}
|
|
153
291
|
|
|
154
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* 1-based line number of the first line containing `needle`, or 1.
|
|
294
|
+
* @param {string} text
|
|
295
|
+
* @param {string} needle
|
|
296
|
+
* @returns {number}
|
|
297
|
+
*/
|
|
298
|
+
function lineOf(text, needle) {
|
|
299
|
+
const lines = text.split(/\r?\n/)
|
|
300
|
+
const i = lines.findIndex(l => l.includes(needle))
|
|
301
|
+
return i < 0 ? 1 : i + 1
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Strip comment-only lines so prose never satisfies a code-level check.
|
|
306
|
+
* @param {string} text
|
|
307
|
+
* @returns {string}
|
|
308
|
+
*/
|
|
155
309
|
function stripComments(text) {
|
|
156
310
|
return text.split(/\r?\n/).filter(l => !/^\s*(?:\/\/|\/\*|\*|#)/.test(l)).join('\n')
|
|
157
311
|
}
|
|
158
312
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Read and parse the repository's top-level package.json, or null.
|
|
315
|
+
* @param {string} repoDir
|
|
316
|
+
* @returns {{ file: string, text: string, json: Record<string, unknown> } | null}
|
|
317
|
+
*/
|
|
318
|
+
function readManifest(repoDir) {
|
|
319
|
+
const file = path.join(repoDir, 'package.json')
|
|
320
|
+
try { return { file, text: fs.readFileSync(file, 'utf8'), json: JSON.parse(fs.readFileSync(file, 'utf8')) } } catch { return null }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* M1 — the local-gate false green. Two independent causes, both reported under
|
|
325
|
+
* this one seam (leg B's card spelled the pair `C3`):
|
|
326
|
+
* (a) a `@deepseek-ai/*` dev/test dependency pinned on the `0.1.5-alpha.*`
|
|
327
|
+
* line, so local typecheck cannot see the deletions later in the span;
|
|
328
|
+
* (b) a `tsconfig` `paths` alias pointing at a checkout directory that does
|
|
329
|
+
* not exist, where TypeScript silently falls back to node_modules.
|
|
330
|
+
* @param {string} repoDir
|
|
331
|
+
* @param {Set<string>} imports bare `@deepseek-ai/*` specifiers the repo imports
|
|
332
|
+
*/
|
|
333
|
+
function checkStaleTypeLine(repoDir, imports) {
|
|
161
334
|
const hits = []
|
|
162
|
-
const
|
|
163
|
-
|
|
335
|
+
const manifest = readManifest(repoDir)
|
|
336
|
+
if (manifest) {
|
|
337
|
+
const dev = manifest.json.devDependencies && typeof manifest.json.devDependencies === 'object' ? manifest.json.devDependencies : {}
|
|
338
|
+
for (const [dep, spec] of Object.entries(dev)) {
|
|
339
|
+
if (!dep.startsWith('@deepseek-ai/')) continue
|
|
340
|
+
if (typeof spec !== 'string') continue
|
|
341
|
+
// Exact-ish pins on the alpha line only: a deliberate range that still
|
|
342
|
+
// covers the span (`>=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0`) is fine.
|
|
343
|
+
if (!/^[\^~]?0\.1\.5-alpha\.[12]$/.test(spec.trim())) continue
|
|
344
|
+
hits.push({
|
|
345
|
+
seam: 'M1', severity: 'error', file: manifest.file, line: lineOf(manifest.text, `"${dep}"`),
|
|
346
|
+
snippet: `"${dep}": "${spec}"`,
|
|
347
|
+
detail: `dev/test types are pinned at the 0.1.5-alpha line (${spec}) → local typecheck cannot see the rc.1 slot catalog (green gate is fake)`,
|
|
348
|
+
})
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const names = (() => { try { return fs.readdirSync(repoDir).filter(f => /^tsconfig.*\.json$/.test(f)) } catch { return [] } })()
|
|
352
|
+
for (const f of names) {
|
|
164
353
|
const full = path.join(repoDir, f)
|
|
165
354
|
let json
|
|
166
355
|
try { json = JSON.parse(fs.readFileSync(full, 'utf8')) } catch { continue }
|
|
@@ -183,7 +372,7 @@ function checkTsconfigPaths(repoDir, imports) {
|
|
|
183
372
|
const resolved = path.resolve(base, probe)
|
|
184
373
|
if (!fs.existsSync(resolved)) {
|
|
185
374
|
hits.push({
|
|
186
|
-
seam: 'M1', severity: 'error', file:
|
|
375
|
+
seam: 'M1', severity: 'error', file: full, line: 1,
|
|
187
376
|
snippet: `"${alias}": ["${t}"]`,
|
|
188
377
|
detail: `resolves to ${resolved} which does not exist → TypeScript silently falls back to node_modules (green gate is fake)`,
|
|
189
378
|
})
|
|
@@ -194,11 +383,37 @@ function checkTsconfigPaths(repoDir, imports) {
|
|
|
194
383
|
return hits
|
|
195
384
|
}
|
|
196
385
|
|
|
386
|
+
/**
|
|
387
|
+
* P1 — the peer band must keep its second segment. `>=0.1.2-rc.1 <0.2.0` alone
|
|
388
|
+
* rejects `0.1.5-rc.1` under npm semver's prerelease-tuple rule.
|
|
389
|
+
* @param {string} repoDir
|
|
390
|
+
*/
|
|
391
|
+
function checkPeerBand(repoDir) {
|
|
392
|
+
/** @type {Hit[]} */
|
|
393
|
+
const hits = []
|
|
394
|
+
const manifest = readManifest(repoDir)
|
|
395
|
+
if (!manifest) return hits
|
|
396
|
+
const peers = manifest.json.peerDependencies && typeof manifest.json.peerDependencies === 'object' ? manifest.json.peerDependencies : {}
|
|
397
|
+
for (const [dep, spec] of Object.entries(peers)) {
|
|
398
|
+
if (!dep.startsWith('@deepseek-ai/dsh-')) continue
|
|
399
|
+
if (typeof spec !== 'string') continue
|
|
400
|
+
const text = spec.trim()
|
|
401
|
+
if (!/0\.1\.2-rc\.1/.test(text)) continue
|
|
402
|
+
if (/0\.1\.5-alpha\.1/.test(text) || /0\.1\.5-rc\.1/.test(text)) continue
|
|
403
|
+
hits.push({
|
|
404
|
+
seam: 'P1', severity: 'error', file: manifest.file, line: lineOf(manifest.text, `"${dep}"`),
|
|
405
|
+
snippet: `"${dep}": "${text}"`,
|
|
406
|
+
detail: 'peer band lost its `>=0.1.5-alpha.1 <0.2.0` segment → 0.1.5-rc.1 is rejected by semver\'s prerelease-tuple rule (measured false on semver 7.8.5)',
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
return hits
|
|
410
|
+
}
|
|
411
|
+
|
|
197
412
|
/**
|
|
198
413
|
* Scan one repo.
|
|
199
414
|
* @param {string} repoDir
|
|
200
415
|
* @param {{ seams?: string[] }} [options]
|
|
201
|
-
* @returns {{ repo: string, scannedAt: string, files: number, hits:
|
|
416
|
+
* @returns {{ repo: string, scannedAt: string, files: number, hits: Hit[], bySeam: Record<string, number> }}
|
|
202
417
|
*/
|
|
203
418
|
export function scanRepo(repoDir, options = {}) {
|
|
204
419
|
const wanted = options.seams && options.seams.length ? new Set(options.seams) : null
|
|
@@ -213,7 +428,8 @@ export function scanRepo(repoDir, options = {}) {
|
|
|
213
428
|
const lines = text.split(/\r?\n/)
|
|
214
429
|
for (const seam of SEAMS) {
|
|
215
430
|
if (wanted && !wanted.has(seam.id)) continue
|
|
216
|
-
if (seam.
|
|
431
|
+
if (seam.test === null) continue // structured or card-only seam
|
|
432
|
+
if (STRUCTURED.has(seam.id)) continue // handled separately (structured, not regex)
|
|
217
433
|
if (seam.fileCheck && !seam.fileCheck(text, file)) continue
|
|
218
434
|
if (seam.fileCheck2 && !seam.fileCheck2(text, file)) continue
|
|
219
435
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -238,19 +454,28 @@ export function scanRepo(repoDir, options = {}) {
|
|
|
238
454
|
}
|
|
239
455
|
}
|
|
240
456
|
}
|
|
241
|
-
if (!wanted || wanted.has('M1')) hits.push(...
|
|
457
|
+
if (!wanted || wanted.has('M1')) hits.push(...checkStaleTypeLine(repoDir, imports))
|
|
458
|
+
if (!wanted || wanted.has('P1')) hits.push(...checkPeerBand(repoDir))
|
|
459
|
+
/** @type {Record<string, number>} */
|
|
242
460
|
const bySeam = {}
|
|
243
461
|
for (const h of hits) bySeam[h.seam] = (bySeam[h.seam] || 0) + 1
|
|
244
462
|
return { repo: repoDir, scannedAt: new Date().toISOString(), files, hits, bySeam }
|
|
245
463
|
}
|
|
246
464
|
|
|
247
|
-
/**
|
|
465
|
+
/**
|
|
466
|
+
* Human-readable rendering. Error group first, advisory seams after.
|
|
467
|
+
* @param {{ repo: string, files: number, hits: Hit[] }} report
|
|
468
|
+
* @returns {string}
|
|
469
|
+
*/
|
|
248
470
|
export function render(report) {
|
|
249
471
|
const L = []
|
|
250
472
|
L.push(`# scan-0.1.5 · ${report.repo}`)
|
|
251
473
|
L.push(`files scanned: ${report.files} · hits: ${report.hits.length}`)
|
|
252
|
-
|
|
253
|
-
|
|
474
|
+
const order = ['S3', 'S8', 'S9', 'M1', 'S4', 'S5', 'S6', 'C1', 'C2', 'P1', 'S7', 'S2', 'S1', 'S10', 'C4', 'C5', 'H1', 'H2', 'H4']
|
|
475
|
+
if (!report.hits.length) {
|
|
476
|
+
L.push('no seam hits — still verify with a real-host smoke AND a real browser assertion for the client half')
|
|
477
|
+
L.push('(this scanner is necessary, not sufficient: the breakage this corridor covers is silent)')
|
|
478
|
+
}
|
|
254
479
|
for (const id of order) {
|
|
255
480
|
const group = report.hits.filter(h => h.seam === id)
|
|
256
481
|
if (!group.length) continue
|
|
@@ -261,10 +486,18 @@ export function render(report) {
|
|
|
261
486
|
for (const h of group.slice(0, 12)) L.push(` ${path.relative(process.cwd(), h.file)}:${h.line} ${h.snippet}`)
|
|
262
487
|
if (group.length > 12) L.push(` ... ${group.length - 12} more`)
|
|
263
488
|
}
|
|
489
|
+
L.push('')
|
|
490
|
+
L.push('H3 [info] card-only: the rc.1 additive capabilities are listed on the version card and are never auto-detected.')
|
|
264
491
|
return L.join('\n')
|
|
265
492
|
}
|
|
266
493
|
|
|
494
|
+
/**
|
|
495
|
+
* CLI entry point.
|
|
496
|
+
* @param {string[]} argv
|
|
497
|
+
* @returns {number} the process exit code.
|
|
498
|
+
*/
|
|
267
499
|
export function main(argv) {
|
|
500
|
+
/** @type {{ repo: string, json: string | null, seams: string[] | null, quiet: boolean }} */
|
|
268
501
|
const args = { repo: process.cwd(), json: null, seams: null, quiet: false }
|
|
269
502
|
for (let i = 0; i < argv.length; i++) {
|
|
270
503
|
const a = argv[i]
|
|
@@ -272,7 +505,7 @@ export function main(argv) {
|
|
|
272
505
|
else if (a === '--json') args.json = argv[++i]
|
|
273
506
|
else if (a === '--seams') args.seams = String(argv[++i]).split(',').map(s => s.trim()).filter(Boolean)
|
|
274
507
|
else if (a === '--quiet') args.quiet = true
|
|
275
|
-
else if (a === '--help' || a === '-h') { console.log('usage: node scan-0.1.5.mjs [--repo <path>] [--json <out.json>] [--seams S3,
|
|
508
|
+
else if (a === '--help' || a === '-h') { console.log('usage: node scan-0.1.5.mjs [--repo <path>] [--json <out.json>] [--seams S3,C1] [--quiet]'); return 0 }
|
|
276
509
|
else { console.error(`unknown argument: ${a}`); return 2 }
|
|
277
510
|
}
|
|
278
511
|
const repoDir = path.resolve(args.repo)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-upgrade",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Plugin-author upgrade skill for DeepSeek Harness:
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Plugin-author upgrade skill for DeepSeek Harness: one package, one corridor index - the scanner detects the caller peer band and routes to the matching closed corridor card (0.1.3-alpha.1 -> 0.1.5-rc.1 as legs A+B, and 0.1.5-rc.2 -> 0.1.6-alpha.2 as leg C), with a zero-dependency seam scanner shipped as a bundle skill and an npx CLI.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/PerryLink/dsh-plugin-upgrade.git"
|
|
@@ -35,16 +35,17 @@
|
|
|
35
35
|
"lib",
|
|
36
36
|
"scripts",
|
|
37
37
|
"skills",
|
|
38
|
+
"docs",
|
|
38
39
|
"cordis.patch.yml",
|
|
39
40
|
"CHANGELOG.md",
|
|
40
41
|
"SECURITY.md",
|
|
41
42
|
"AGENTS.md",
|
|
42
43
|
"THIRD_PARTY_NOTICES.md",
|
|
43
44
|
"README.md",
|
|
44
|
-
"README
|
|
45
|
-
"README
|
|
46
|
-
"README
|
|
47
|
-
"README
|
|
45
|
+
"README-zh.md",
|
|
46
|
+
"README-es.md",
|
|
47
|
+
"README-pt.md",
|
|
48
|
+
"README-hi.md",
|
|
48
49
|
"LICENSE"
|
|
49
50
|
],
|
|
50
51
|
"sideEffects": false,
|
|
@@ -53,6 +54,45 @@
|
|
|
53
54
|
"patch": "./cordis.patch.yml"
|
|
54
55
|
}
|
|
55
56
|
},
|
|
57
|
+
"dshWorkshop": {
|
|
58
|
+
"schema": "omdsh-workshop-package/v1",
|
|
59
|
+
"type": "plugin",
|
|
60
|
+
"integration": {
|
|
61
|
+
"protocol": "harness-profile",
|
|
62
|
+
"artifact": "cordis.patch.yml"
|
|
63
|
+
},
|
|
64
|
+
"install": {
|
|
65
|
+
"mode": "transactional",
|
|
66
|
+
"adapter": "profile-bundle",
|
|
67
|
+
"failurePolicy": "generation-rollback",
|
|
68
|
+
"touchesCurrentBeforeActivation": false
|
|
69
|
+
},
|
|
70
|
+
"lifecycle": {
|
|
71
|
+
"activation": "restart-profile",
|
|
72
|
+
"dispose": "supported"
|
|
73
|
+
},
|
|
74
|
+
"permissions": [
|
|
75
|
+
"filesystem:read"
|
|
76
|
+
],
|
|
77
|
+
"compatibility": {
|
|
78
|
+
"dshVersions": [
|
|
79
|
+
"0.1.2-rc.1",
|
|
80
|
+
"0.1.5-rc.2"
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"capability": {
|
|
84
|
+
"id": "plugin-upgrade",
|
|
85
|
+
"kind": "service",
|
|
86
|
+
"invocation": "ctx.skills.get('plugin-upgrade') resolves a complete SkillDefinition",
|
|
87
|
+
"expected": "the session skill catalog lists plugin-upgrade and the loaded body carries the merged 0.1.3-alpha.1 -> 0.1.5-rc.1 corridor card with a directory resourceBase"
|
|
88
|
+
},
|
|
89
|
+
"evidence": {
|
|
90
|
+
"install": null,
|
|
91
|
+
"failureIsolation": null,
|
|
92
|
+
"hotReload": null,
|
|
93
|
+
"remove": null
|
|
94
|
+
}
|
|
95
|
+
},
|
|
56
96
|
"keywords": [
|
|
57
97
|
"dsh",
|
|
58
98
|
"dsh-plugin",
|
|
@@ -63,7 +103,8 @@
|
|
|
63
103
|
"migration",
|
|
64
104
|
"skill",
|
|
65
105
|
"version-card",
|
|
66
|
-
"scanner"
|
|
106
|
+
"scanner",
|
|
107
|
+
"client-slots"
|
|
67
108
|
],
|
|
68
109
|
"engines": {
|
|
69
110
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -71,16 +112,20 @@
|
|
|
71
112
|
"packageManager": "pnpm@11.7.0",
|
|
72
113
|
"peerDependencies": {
|
|
73
114
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
74
|
-
"@deepseek-ai/dsh-skill": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0",
|
|
115
|
+
"@deepseek-ai/dsh-skill": ">=0.1.2-rc.1 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-0 <0.2.0",
|
|
75
116
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
76
117
|
},
|
|
77
118
|
"devDependencies": {
|
|
78
119
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
79
|
-
"@deepseek-ai/dsh-skill": "0.1.5-rc.
|
|
80
|
-
"@deepseek-ai/schemastery": "^3.18.2"
|
|
120
|
+
"@deepseek-ai/dsh-skill": "0.1.5-rc.2",
|
|
121
|
+
"@deepseek-ai/schemastery": "^3.18.2",
|
|
122
|
+
"@types/node": "^22.0.0",
|
|
123
|
+
"typescript": "^5.9.0"
|
|
81
124
|
},
|
|
82
125
|
"scripts": {
|
|
83
126
|
"test": "node --test \"test/*.test.mjs\"",
|
|
127
|
+
"check": "tsc -p tsconfig.check.json",
|
|
128
|
+
"typecheck:ci": "tsc -p tsconfig.check.ci.json",
|
|
84
129
|
"scan": "node scripts/scan-0.1.5.mjs",
|
|
85
130
|
"check:readmes": "node scripts/check-readme-sync.mjs",
|
|
86
131
|
"verify:self-contained": "node scripts/verify-self-contained.mjs",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
// Five-language README sync gate: every README must carry the same number of
|
|
2
3
|
// `## ` sections as the English source and state the install command.
|
|
3
4
|
// Usage: node scripts/check-readme-sync.mjs
|
|
@@ -6,14 +7,16 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
6
7
|
import { fileURLToPath } from 'node:url'
|
|
7
8
|
|
|
8
9
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
-
const FILES = ['README.md', 'README
|
|
10
|
+
const FILES = ['README.md', 'README-zh.md', 'README-es.md', 'README-pt.md', 'README-hi.md']
|
|
10
11
|
const INSTALL_COMMAND = 'dsh plugin --profile web add dsh-plugin-upgrade'
|
|
11
12
|
const failures = []
|
|
13
|
+
/** @param {string} file @returns {string} */
|
|
12
14
|
const read = (file) => {
|
|
13
15
|
const p = join(root, file)
|
|
14
16
|
if (!existsSync(p)) { failures.push(`${file} is missing`); return '' }
|
|
15
17
|
return readFileSync(p, 'utf8')
|
|
16
18
|
}
|
|
19
|
+
/** @param {string} text @returns {number} */
|
|
17
20
|
const sectionCount = text => (text.match(/^## /gmu) ?? []).length
|
|
18
21
|
|
|
19
22
|
const contents = FILES.map(read)
|
package/scripts/scan-0.1.5.mjs
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
3
|
/**
|
|
3
|
-
* CLI wrapper for the
|
|
4
|
+
* CLI wrapper for the corridor-index router (one package, several closed corridors).
|
|
4
5
|
*
|
|
5
6
|
* Usage:
|
|
6
|
-
* node scripts/scan-0.1.5.mjs [--repo <path>] [--json <out.json>] [--seams
|
|
7
|
+
* node scripts/scan-0.1.5.mjs [--repo <path>] [--span legC|legAB|<span>] [--json <out.json>] [--seams E1,E3] [--quiet]
|
|
7
8
|
* npx --package dsh-plugin-upgrade dsh-plugin-upgrade-scan --repo <path>
|
|
8
9
|
*
|
|
10
|
+
* The wrapper only routes: it reads the target repository's declared dsh band (or
|
|
11
|
+
* an explicit --span) and hands the SAME argv to that corridor's own catalog
|
|
12
|
+
* module, so every corridor keeps its own evidence-bound seam array and its own
|
|
13
|
+
* main(). An unknown band routes to the older corridor (legAB), which is what a
|
|
14
|
+
* repository predating 0.1.6 needs. Nothing here reaches the network.
|
|
15
|
+
*
|
|
9
16
|
* Exit codes: 0 = no error-severity hit, 1 = at least one error-severity hit,
|
|
10
|
-
* 2 = usage or scan failure.
|
|
11
|
-
* so the plugin, the CLI and the tests share one seam catalog.
|
|
17
|
+
* 2 = usage or scan failure.
|
|
12
18
|
*/
|
|
13
|
-
import {
|
|
19
|
+
import { resolveCorridor, spanFromArgv, repoFromArgv, loadCatalog } from '../lib/route.mjs'
|
|
20
|
+
|
|
21
|
+
const argv = process.argv.slice(2)
|
|
22
|
+
const corridor = resolveCorridor({ repoDir: repoFromArgv(argv), span: spanFromArgv(argv) })
|
|
23
|
+
const catalog = await loadCatalog(corridor)
|
|
24
|
+
|
|
25
|
+
if (!argv.includes('--quiet')) {
|
|
26
|
+
process.stderr.write(`corridor: ${corridor.id} (${corridor.span}) -> ${corridor.catalog}\n`)
|
|
27
|
+
}
|
|
14
28
|
|
|
15
|
-
process.exit(main(
|
|
29
|
+
process.exit(catalog.main(argv))
|
package/scripts/sweep-all.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
// Sweep every plugin repo under a workspace root and write the evidence table.
|
|
3
|
+
// Maintainer tool: it ships in the tarball because `scripts/` does, but neither
|
|
4
|
+
// the plugin entry nor the skill body calls it.
|
|
2
5
|
// Usage: node sweep-all.mjs [<workspaceRoot>] [<out.md>]
|
|
3
6
|
import fs from 'node:fs'
|
|
4
7
|
import path from 'node:path'
|
|
@@ -6,7 +9,8 @@ import { scanRepo } from '../lib/scan.mjs'
|
|
|
6
9
|
|
|
7
10
|
const ROOT = path.resolve(process.argv[2] || '.')
|
|
8
11
|
const OUT = path.resolve(process.argv[3] || path.join(ROOT, 'scan-0.1.5-sweep.md'))
|
|
9
|
-
|
|
12
|
+
// Third-party repositories that happen to live in the same workspace.
|
|
13
|
+
const SKIP = new Set(['adp-list', 'audit-dsh-infinite-gen-2', 'pan17-dsh-wechat', 'dsh-personal-directive'])
|
|
10
14
|
const dirs = fs.readdirSync(ROOT, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
|
|
11
15
|
.filter(n => !n.startsWith('_') && !n.startsWith('.') && !SKIP.has(n))
|
|
12
16
|
.filter(n => fs.existsSync(path.join(ROOT, n, 'package.json'))).sort()
|
|
@@ -23,7 +27,7 @@ L.push('# scan-0.1.5 · workspace sweep evidence')
|
|
|
23
27
|
L.push('')
|
|
24
28
|
L.push(`Generated ${new Date().toISOString()} · scanner \`scripts/scan-0.1.5.mjs\` · workspace \`${ROOT}\``)
|
|
25
29
|
L.push('')
|
|
26
|
-
L.push('
|
|
30
|
+
L.push('Error-severity hits mean the repo would silently break (or already fails to resolve) somewhere on the merged `0.1.3-alpha.1 → 0.1.5-rc.1` corridor. Warn/info hits are heuristic leads for manual review.')
|
|
27
31
|
L.push('')
|
|
28
32
|
L.push('| repo | files | errors | warns | error seams |')
|
|
29
33
|
L.push('|---|---|---|---|---|')
|