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/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 → 0.1.5-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: typecheck passing is NOT evidence of adaptation. Two classes of
7
- * failure survive a green local gate — (a) seams the published type line hides
8
- * because the repo compiles against stale types, and (b) seams whose tests are
9
- * mocked against the old shape. This scanner reports `file:line` facts for ten
10
- * seams measured against 40 real plugin repos during the 2026-09-09 wave, and
11
- * treats the "tsconfig silently resolves to the wrong types" case (M1) as a
12
- * first-class defect, not a warning.
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,S8,M1] [--quiet]
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: evidence for every seam lives in the 2026-09-09 batch report
21
- * (40 plugin repos) and the per-repo cards produced by that wave. The scanner
22
- * ships in this package so a plugin author can re-measure their own repo; it
23
- * imports nothing outside Node's standard library.
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
- /** @typedef {{ id: string, title: string, severity: 'error'|'warn'|'info', action: string, test: RegExp }} Seam */
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: 'tsconfig 的 checkout 路径解析失败 → typecheck 静默回退',
123
+ title: '本地门禁编译的是过期类型线 → typecheck 静默回退(假绿)',
77
124
  severity: 'error',
78
- action: 'tsconfig `paths` 指向不存在的目录时 TypeScript 会静默回退到 node_modules 的已发布类型,本地门禁变成假绿。路径应为 `../../../../deepseek-harness/packages/...`(相对仓库根)。',
79
- test: /__tsconfig_paths_probe__/,
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
- /** Strip comment-only lines so prose never satisfies a code-level check. */
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
- /** M1: resolve every `paths` entry; a missing target means silent fallback. */
160
- function checkTsconfigPaths(repoDir, imports) {
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 files = fs.readdirSync(repoDir).filter(f => /^tsconfig.*\.json$/.test(f))
163
- for (const f of files) {
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: path.join(repoDir, f), line: 1,
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: any[], bySeam: Record<string, number> }}
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.id === 'M1') continue // handled separately (structured, not regex)
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(...checkTsconfigPaths(repoDir, imports))
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
- /** Human-readable rendering. */
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
- if (!report.hits.length) { L.push('no seam hits still verify with real-host smoke (this scanner is necessary, not sufficient)'); return L.join('\n') }
253
- const order = ['S3', 'S8', 'S9', 'M1', 'S4', 'S5', 'S6', 'S7', 'S2', 'S1', 'S10']
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,S8,M1] [--quiet]'); return 0 }
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.3",
4
- "description": "Plugin-author upgrade skill for DeepSeek Harness: a version-locked 0.1.3-alpha.1 -> 0.1.5-alpha.1 version card plus a zero-dependency seam scanner (V3 session format, assistant/message.stream, SessionHandleReadResult, ctx.agent, Inbox, SubprocessHandle.pid, SystemPrompt persona, PTC rename, EpochHeader.system, and the tsconfig stale-path false green), packaged as a bundle skill and an npx CLI.",
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.zh.md",
45
- "README.es.md",
46
- "README.pt.md",
47
- "README.hi.md",
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.1",
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
  // Print the CHANGELOG.md section for one version, for GitHub Release notes.
2
3
  // Usage: node scripts/changelog-section.mjs <x.y.z>
3
4
  import { readFileSync } from 'node:fs'
@@ -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.zh.md', 'README.es.md', 'README.pt.md', 'README.hi.md']
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)
@@ -1,15 +1,29 @@
1
1
  #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
2
3
  /**
3
- * CLI wrapper for the packaged seam scanner.
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 M1,S3] [--quiet]
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. The implementation lives in ../lib/scan.mjs
11
- * so the plugin, the CLI and the tests share one seam catalog.
17
+ * 2 = usage or scan failure.
12
18
  */
13
- import { main } from '../lib/scan.mjs'
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(process.argv.slice(2)))
29
+ process.exit(catalog.main(argv))
@@ -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
- const SKIP = new Set(['adp-list', 'audit-dsh-infinite-gen-2', 'pan17-dsh-wechat', 'dsh-autotier', 'dsh-personal-directive'])
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('All repos below were adapted to `0.1.5-alpha.1` by the 2026-09-09 wave. Error-severity hits are expected to be **zero**; warn-severity hits are heuristic leads for manual review (S1/S2/S10).')
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('|---|---|---|---|---|')