@aiwayds/dsh-dcp 0.6.0 → 0.8.0

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.en.md CHANGED
@@ -3,6 +3,8 @@
3
3
  Deterministic context-compaction backend for dsh (DeepSeek Harness): **context
4
4
  compaction without an LLM call**, works out of the box.
5
5
 
6
+ **Requires dsh >= 0.1.2-rc.1** — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the `latest`/`next` dist-tags at runtime). **The alpha line is no longer supported.**
7
+
6
8
  > [简体中文](README.md) · **English**
7
9
 
8
10
  ## Why
@@ -103,6 +105,28 @@ npx dsh-dcp-setup # safe: date-stamped backup → append-only
103
105
  > mount it. The web profile does not bundle the TUI, so it keeps the official
104
106
  > backend and is unaffected.
105
107
 
108
+ ## Uninstall
109
+
110
+ **Bundle path** (`dsh plugin add`, or listed under `bundles`):
111
+
112
+ ```sh
113
+ dsh plugin --profile <name> remove @aiwayds/dsh-dcp
114
+ ```
115
+
116
+ The host reconciles automatically: the bundles entry is spliced, the patch layer drops with the package, and the stock LLM summarizer (`compaction-basic`) re-enables.
117
+
118
+ **Setup-script path** (the patch block `npx dsh-dcp-setup` wrote):
119
+
120
+ ```bash
121
+ npx dsh-dcp-setup --remove # default home patch
122
+ npx dsh-dcp-setup --remove --profile tui # a named profile
123
+ npx dsh-dcp-setup --remove /path/to/cordis.patch.yml
124
+ ```
125
+
126
+ `--remove` strips only the setup-written mount block (including any config you tuned), with the same date-stamped backup before modifying; hand-written mounts are left alone; the file is deleted when nothing else remains. Two WARN lines matter: a leftover `compaction-basic` disable entry keeps the stock summarizer off (remove it by hand if it was only for dsh-dcp), and another dsh-dcp mount in the same file is untouched.
127
+
128
+ Removing the package without the reverse step leaves the mount pointing at the vanished absolute entry path, and the profile boot fails with module-not-found.
129
+
106
130
  ## /dcp command
107
131
 
108
132
  | Command | Effect |
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  dsh(DeepSeek Harness)的确定性压缩后端:**上下文压缩不调 LLM**,开箱即用。
4
4
 
5
+ **要求 dsh >= 0.1.2-rc.1** — 本插件只跟随 dsh RC/stable 线(CI 与发版在运行时解析 latest/next 中更新的 dist-tag)。**不再支持 alpha 线。**
6
+
5
7
  > **简体中文** · [English](README.en.md)
6
8
 
7
9
  ## 为什么做
@@ -82,6 +84,28 @@ npx dsh-dcp-setup # 安全脚本:带日期备份 → 只追
82
84
 
83
85
  > dsh-dcp 挂在 dsh 的压缩接口上,只对挂载了它的 profile 生效。web profile 没挂 tui,继续用官方压缩,不受影响。
84
86
 
87
+ ## 卸载
88
+
89
+ **Bundle 方式**(`dsh plugin add` 或列在 `bundles`):
90
+
91
+ ```sh
92
+ dsh plugin --profile <name> remove @aiwayds/dsh-dcp
93
+ ```
94
+
95
+ 宿主自动收敛:bundles 条目移除、patch 层随包消失,官方 LLM 压缩后端 `compaction-basic` 自动恢复。
96
+
97
+ **setup 脚本方式**(`npx dsh-dcp-setup` 写入的 patch 块):
98
+
99
+ ```bash
100
+ npx dsh-dcp-setup --remove # 默认 home patch
101
+ npx dsh-dcp-setup --remove --profile tui # 指定 profile
102
+ npx dsh-dcp-setup --remove /path/to/cordis.patch.yml
103
+ ```
104
+
105
+ `--remove` 只删除 setup 写入的挂载块(含你调过的 config),修改前同样带日期备份;手工写的挂载块不受影响;删完文件为空时自动删除文件。两条 WARN 要留意:文件里若还留有 `compaction-basic` 的 disable 条目,官方压缩后端会保持关闭 —— 不是给 dsh-dcp 用的就手动删掉;同一文件里另一处 dsh-dcp 挂载不会被碰。
106
+
107
+ 不执行反向步骤直接卸包,patch 里的绝对路径会指向已消失的安装目录,profile 启动将以 module-not-found 失败。
108
+
85
109
  ## /dcp 命令
86
110
 
87
111
  | 命令 | 作用 |
package/cordis.patch.yml CHANGED
@@ -7,6 +7,7 @@
7
7
  # cordis.patch.yml editing.
8
8
 
9
9
  - id: compaction-basic
10
+ name: '@deepseek-ai/dsh-compaction-basic'
10
11
  disabled: true
11
12
  - insert:
12
13
  - id: dsh-dcp
package/lib/setup.js CHANGED
@@ -11,8 +11,12 @@
11
11
  */
12
12
  import { readdirSync, readFileSync } from 'node:fs'
13
13
 
14
- /** The patch entry that disables the default LLM summarizer. */
14
+ /** The patch entry that disables the default LLM summarizer. The `name` is
15
+ * a guard, not an override: if a future host renames or drops the row, the
16
+ * entry is skipped with a loader warning instead of silently disabling an
17
+ * unrelated component (the same pattern dsh-tui-pi's patch uses). */
15
18
  const DISABLE_ENTRY = `- id: compaction-basic
19
+ name: '@deepseek-ai/dsh-compaction-basic'
16
20
  disabled: true`
17
21
 
18
22
  /** The dsh-dcp mount block, one YAML list item, appended to the file. */
@@ -93,3 +97,65 @@ export function backupStamp(date = new Date()) {
93
97
  const p = (n) => String(n).padStart(2, '0')
94
98
  return `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}-${p(date.getHours())}${p(date.getMinutes())}`
95
99
  }
100
+
101
+ /** The marker comment every setup-written mount block starts with. */
102
+ const MOUNT_MARKER = 'added by @aiwayds/dsh-dcp setup'
103
+
104
+ /**
105
+ * Reverse of planPatch/mountBlock: remove the setup-appended dsh-dcp mount
106
+ * block — the marker comment, the disable entry it added, and the insert
107
+ * item including any config the user tuned inside it — from one patch
108
+ * file's text. Hand-written mounts without the marker are NOT touched (the
109
+ * human wrote it; the human removes it); user entries after the block are
110
+ * preserved byte-for-byte.
111
+ *
112
+ * Uninstalling the package without this reverse step leaves the mount
113
+ * pointing at the vanished absolute entry path, which fails the profile's
114
+ * whole boot with a module-not-found error.
115
+ *
116
+ * @param {string|undefined} text - the patch file content.
117
+ * @returns {{ removed: boolean, text: string, note?: string }} `text` with
118
+ * the block gone ('' when nothing else remains — the caller may delete the
119
+ * file); `removed: false` leaves `text` untouched and sets `note`.
120
+ */
121
+ export function planRemoval(text) {
122
+ if (!text) return { removed: false, text: text ?? '' }
123
+ const lines = text.split('\n')
124
+ const marker = lines.findIndex((line) => line.includes(MOUNT_MARKER))
125
+ if (marker === -1) {
126
+ return { removed: false, text, note: 'no setup-written dsh-dcp mount block found (hand-written mounts are left alone)' }
127
+ }
128
+ let i = marker + 1
129
+ let end = lines.length
130
+ let sawInsert = false
131
+ while (i < lines.length) {
132
+ const line = lines[i]
133
+ if (line === '') { i++; continue }
134
+ if (line.startsWith('- ')) {
135
+ // One top-level list item: the entry line plus its indented body (and
136
+ // any blank lines inside it the user added while tuning config). The
137
+ // split artifact after a final newline is EOF, not a blank to consume.
138
+ let j = i + 1
139
+ while (j < lines.length && (lines[j] === '' ? j < lines.length - 1 : lines[j].startsWith(' ') || lines[j].startsWith('\t'))) j++
140
+ if (line.startsWith('- insert')) {
141
+ sawInsert = true
142
+ end = j
143
+ break
144
+ }
145
+ i = j
146
+ continue
147
+ }
148
+ // Anything else at column 0 (a user comment or entry) ends our block.
149
+ end = i
150
+ break
151
+ }
152
+ if (!sawInsert) {
153
+ return { removed: false, text, note: 'the setup marker was found but the dsh-dcp insert item is missing — leaving the file untouched' }
154
+ }
155
+ let start = marker
156
+ // The block was appended with one leading blank line; take it so repeated
157
+ // mount/unmount cycles do not accumulate blank runs.
158
+ if (start > 0 && lines[start - 1].trim() === '') start -= 1
159
+ const remaining = [...lines.slice(0, start), ...lines.slice(end)].join('\n')
160
+ return { removed: true, text: remaining.trim() === '' ? '' : remaining }
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-dcp",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Deterministic context-pruning compaction backend for dsh (DeepSeek Harness) — zero-LLM summaries, /dcp command, works out of the box. Design references Opencode-DCP/opencode-dynamic-context-pruning.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -52,16 +52,16 @@
52
52
  "dependencies": {},
53
53
  "peerDependencies": {
54
54
  "@deepseek-ai/cordis": "^4.0.2",
55
- "@deepseek-ai/dsh-agent": ">=0.1.2-alpha.3",
56
- "@deepseek-ai/dsh-brand": ">=0.1.2-alpha.3",
57
- "@deepseek-ai/dsh-commands": ">=0.1.2-alpha.3",
58
- "@deepseek-ai/dsh-compaction": ">=0.1.2-alpha.3",
59
- "@deepseek-ai/dsh-compaction-basic": ">=0.1.2-alpha.3",
60
- "@deepseek-ai/dsh-compaction-tool-result-pruner": ">=0.1.2-alpha.3",
61
- "@deepseek-ai/dsh-invariants": ">=0.1.2-alpha.3",
62
- "@deepseek-ai/dsh-llm": ">=0.1.2-alpha.3",
63
- "@deepseek-ai/dsh-session": ">=0.1.2-alpha.3",
64
- "@deepseek-ai/dsh-token-meter": ">=0.1.2-alpha.3",
55
+ "@deepseek-ai/dsh-agent": ">=0.1.2-rc.1",
56
+ "@deepseek-ai/dsh-brand": ">=0.1.2-rc.1",
57
+ "@deepseek-ai/dsh-commands": ">=0.1.2-rc.1",
58
+ "@deepseek-ai/dsh-compaction": ">=0.1.2-rc.1",
59
+ "@deepseek-ai/dsh-compaction-basic": ">=0.1.2-rc.1",
60
+ "@deepseek-ai/dsh-compaction-tool-result-pruner": ">=0.1.2-rc.1",
61
+ "@deepseek-ai/dsh-invariants": ">=0.1.2-rc.1",
62
+ "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1",
63
+ "@deepseek-ai/dsh-session": ">=0.1.2-rc.1",
64
+ "@deepseek-ai/dsh-token-meter": ">=0.1.2-rc.1",
65
65
  "@deepseek-ai/schemastery": "^3.18.2"
66
66
  },
67
67
  "peerDependenciesMeta": {
@@ -105,33 +105,47 @@
105
105
  "devDependencies": {
106
106
  "@types/node": "^24.0.0",
107
107
  "@deepseek-ai/cordis": "4.0.2",
108
- "@deepseek-ai/dsh-agent": "0.1.2-alpha.3",
109
- "@deepseek-ai/dsh-brand": "0.1.2-alpha.3",
110
- "@deepseek-ai/dsh-commands": "0.1.2-alpha.3",
111
- "@deepseek-ai/dsh-compaction": "0.1.2-alpha.3",
112
- "@deepseek-ai/dsh-compaction-basic": "0.1.2-alpha.3",
113
- "@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.2-alpha.3",
114
- "@deepseek-ai/dsh-invariants": "0.1.2-alpha.3",
115
- "@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
116
- "@deepseek-ai/dsh-session": "0.1.2-alpha.3",
117
- "@deepseek-ai/dsh-token-meter": "0.1.2-alpha.3",
108
+ "@deepseek-ai/dsh-agent": "0.1.2-rc.1",
109
+ "@deepseek-ai/dsh-brand": "0.1.2-rc.1",
110
+ "@deepseek-ai/dsh-commands": "0.1.2-rc.1",
111
+ "@deepseek-ai/dsh-compaction": "0.1.2-rc.1",
112
+ "@deepseek-ai/dsh-compaction-basic": "0.1.2-rc.1",
113
+ "@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.2-rc.1",
114
+ "@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
115
+ "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
116
+ "@deepseek-ai/dsh-session": "0.1.2-rc.1",
117
+ "@deepseek-ai/dsh-token-meter": "0.1.2-rc.1",
118
118
  "@deepseek-ai/schemastery": "3.18.2",
119
119
  "typescript": "~5.9.0"
120
120
  },
121
121
  "overrides": {
122
- "@deepseek-ai/dsh-agent": "0.1.2-alpha.3",
123
- "@deepseek-ai/dsh-brand": "0.1.2-alpha.3",
124
- "@deepseek-ai/dsh-commands": "0.1.2-alpha.3",
125
- "@deepseek-ai/dsh-compaction": "0.1.2-alpha.3",
126
- "@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.2-alpha.3",
127
- "@deepseek-ai/dsh-invariants": "0.1.2-alpha.3",
128
- "@deepseek-ai/dsh-llm": "0.1.2-alpha.3",
129
- "@deepseek-ai/dsh-session": "0.1.2-alpha.3",
130
- "@deepseek-ai/dsh-token-meter": "0.1.2-alpha.3"
122
+ "@deepseek-ai/dsh-agent": "0.1.2-rc.1",
123
+ "@deepseek-ai/dsh-brand": "0.1.2-rc.1",
124
+ "@deepseek-ai/dsh-commands": "0.1.2-rc.1",
125
+ "@deepseek-ai/dsh-compaction": "0.1.2-rc.1",
126
+ "@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.2-rc.1",
127
+ "@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
128
+ "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
129
+ "@deepseek-ai/dsh-session": "0.1.2-rc.1",
130
+ "@deepseek-ai/dsh-token-meter": "0.1.2-rc.1",
131
+ "@deepseek-ai/dsh-scope": "0.1.2-rc.1",
132
+ "@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
133
+ "@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
134
+ "@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
135
+ "@deepseek-ai/dsh-llm-retry": "0.1.2-rc.1"
131
136
  },
132
137
  "dsh": {
133
138
  "bundle": {
134
139
  "patch": "./cordis.patch.yml"
135
140
  }
141
+ },
142
+ "pnpm": {
143
+ "overrides": {
144
+ "@deepseek-ai/dsh-scope": "0.1.2-rc.1",
145
+ "@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
146
+ "@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
147
+ "@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
148
+ "@deepseek-ai/dsh-llm-retry": "0.1.2-rc.1"
149
+ }
136
150
  }
137
151
  }
package/scripts/setup.mjs CHANGED
@@ -20,11 +20,13 @@
20
20
  * node scripts/setup.mjs # home patch
21
21
  * node scripts/setup.mjs --profile tui # tui profile's patch
22
22
  * node scripts/setup.mjs /path/to/cordis.patch.yml
23
+ * node scripts/setup.mjs --remove [...] # reverse: remove the setup-written
24
+ * # mount block (same target forms)
23
25
  */
24
26
  import fs from 'node:fs'
25
27
  import path from 'node:path'
26
28
  import { fileURLToPath } from 'node:url'
27
- import { planPatch, backupStamp, findBundledProfiles } from '../lib/setup.js'
29
+ import { planPatch, planRemoval, hasEntry, isMounted, backupStamp, findBundledProfiles } from '../lib/setup.js'
28
30
 
29
31
  const PKG = '@aiwayds/dsh-dcp'
30
32
  const pkgRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
@@ -36,18 +38,53 @@ function home() {
36
38
 
37
39
  function parseArgs(argv) {
38
40
  const force = argv.includes('--force')
41
+ const remove = argv.includes('--remove')
39
42
  if (argv.includes('--profile')) {
40
43
  const index = argv.indexOf('--profile')
41
44
  const name = argv[index + 1]
42
45
  if (!name) throw new Error('setup: --profile requires a profile name')
43
- return { target: path.join(home(), 'profiles', name, 'cordis.patch.yml'), kind: 'profile', profileName: name, force }
46
+ return { target: path.join(home(), 'profiles', name, 'cordis.patch.yml'), kind: 'profile', profileName: name, force, remove }
44
47
  }
45
48
  const explicit = argv.find((a) => !a.startsWith('-'))
46
- if (explicit) return { target: path.resolve(explicit), kind: 'explicit', force }
47
- return { target: path.join(home(), 'cordis.patch.yml'), kind: 'home', force }
49
+ if (explicit) return { target: path.resolve(explicit), kind: 'explicit', force, remove }
50
+ return { target: path.join(home(), 'cordis.patch.yml'), kind: 'home', force, remove }
51
+ }
52
+
53
+ const { target, kind, profileName, force, remove } = parseArgs(process.argv.slice(2))
54
+
55
+ if (remove) {
56
+ const existed = fs.existsSync(target)
57
+ const text = existed ? fs.readFileSync(target, 'utf8') : undefined
58
+ if (!existed) {
59
+ console.log(`no ${target} — nothing to remove.`)
60
+ process.exit(0)
61
+ }
62
+ const plan = planRemoval(text)
63
+ if (!plan.removed) {
64
+ console.log(`dsh-dcp mount not removed from ${target}.`)
65
+ if (plan.note) console.log(`note: ${plan.note}`)
66
+ process.exit(0)
67
+ }
68
+ const backup = `${target}.bak.${backupStamp()}`
69
+ fs.copyFileSync(target, backup)
70
+ console.log(`backup: ${backup}`)
71
+ if (plan.text === '') {
72
+ fs.rmSync(target)
73
+ console.log(`removed ${target} (the file held nothing else)`)
74
+ } else {
75
+ fs.writeFileSync(target, plan.text)
76
+ console.log(`removed the dsh-dcp mount block from ${target}`)
77
+ }
78
+ if (plan.text !== '' && hasEntry(plan.text, 'compaction-basic')) {
79
+ console.warn('WARN: a compaction-basic entry remains in this patch file — the stock LLM summarizer stays disabled; remove that entry by hand if it was only for dsh-dcp.')
80
+ }
81
+ if (plan.text !== '' && isMounted(plan.text)) {
82
+ console.warn('WARN: another dsh-dcp mount remains in this patch file — leaving it alone.')
83
+ }
84
+ console.log('restart dsh to apply.')
85
+ process.exit(0)
48
86
  }
49
87
 
50
- const { target, kind, profileName, force } = parseArgs(process.argv.slice(2))
51
88
  const existed = fs.existsSync(target)
52
89
  const text = existed ? fs.readFileSync(target, 'utf8') : undefined
53
90
  if (!existed) console.log(`no ${target} — will generate a fresh patch file`)
@@ -12,11 +12,16 @@
12
12
  // as a file: dependency, and a cordis.patch.yml carrying the
13
13
  // disable-compaction-basic + insert-dsh-dcp mount block
14
14
  // 3. pnpm install in the profile
15
- // 4. `dsh --profile smoke --dump-config` must compose dsh-dcp into the tree
16
- // (mount/patch-layer proof)
17
- // 5. a real boot under a timeout must load the plugin tree without a loader
18
- // error (a healthy boot is silent and survives to the kill signal; a
19
- // broken plugin dies within ~1s with the loader error)
15
+ // 4. Phase A: setup.mjs mounts the profile patch (marker block), the
16
+ // composed tree carries dsh-dcp with compaction-basic disabled, then
17
+ // `setup.mjs --remove` strips it and the recomposed tree is back to
18
+ // stock (this is what breaks a profile when the package is removed
19
+ // without the reverse step)
20
+ // 5. Phase B: a hand-written mount pointing into the installed tarball —
21
+ // the true user shape — composes, and a real boot under a timeout must
22
+ // load the plugin tree without a loader error (a healthy boot is silent
23
+ // and survives to the kill signal; a broken plugin dies within ~1s with
24
+ // the loader error)
20
25
  //
21
26
  // The dsh CLI comes from $DSH_BIN if set (e.g. a scratch alpha closure:
22
27
  // DSH_BIN=~/tmp/dsh-alpha-closure/node_modules/@deepseek-ai/dsh/lib/bin.js),
@@ -57,18 +62,9 @@ if (pack.status !== 0 || pack.error) fail('npm pack failed', `${pack.stdout}\n${
57
62
  const tarball = path.join(work, pack.stdout.trim().split('\n').at(-1) ?? '')
58
63
 
59
64
  writeFileSync(path.join(profile, 'cordis.yml'), '# dsh profile root — empty; the tree is composed from the bundle patches\n[]\n')
60
- // The mount layer under test — isomorphic with scripts/setup.mjs's output:
61
- // compaction-basic disabled, dsh-dcp inserted by absolute entry path.
62
- writeFileSync(path.join(profile, 'cordis.patch.yml'), `# scratch smoke profile: dsh-dcp mounted the setup.mjs way
63
- - id: compaction-basic
64
- disabled: true
65
- - insert:
66
- - id: ${entryId}
67
- name: ${path.join(profile, 'node_modules', '@aiwayds', 'dsh-dcp', 'lib', 'index.js')}
68
- config:
69
- thresholdRatio: 0.7
70
- language: zh
71
- `)
65
+ // The patch file starts EMPTY: Phase A below drives the full setup.mjs
66
+ // mount → remove lifecycle against it, then Phase B hand-writes the mount
67
+ // (isomorphic with setup.mjs's output) for the real-boot proof.
72
68
  writeFileSync(path.join(profile, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n')
73
69
  writeFileSync(path.join(profile, 'package.json'), JSON.stringify({
74
70
  name: 'dsh-profile-smoke',
@@ -90,8 +86,47 @@ writeFileSync(path.join(profile, 'package.json'), JSON.stringify({
90
86
  const install = spawnSync('pnpm', ['install'], { cwd: profile, encoding: 'utf8' })
91
87
  if (install.status !== 0 || install.error) fail('pnpm install in the scratch profile failed', `${install.stdout}\n${install.stderr}`)
92
88
 
93
- // Phase 1 — mount proof: the composed tree must carry the dsh-dcp entry, with
94
- // compaction-basic disabled rather than removed-by-default.
89
+ // Phase A — the setup.mjs lifecycle: mount → compose → remove → stock tree.
90
+ // This is the exact pair of commands a standalone user runs (`npx dsh-dcp-setup`
91
+ // and its `--remove`), against the profile patch file they target.
92
+ const patchFile = path.join(profile, 'cordis.patch.yml')
93
+ const setup = spawnSync(process.execPath, [path.join(repoRoot, 'scripts', 'setup.mjs'), patchFile], { encoding: 'utf8' })
94
+ if (setup.status !== 0 || setup.error) fail('setup.mjs mount failed', `${setup.stdout}\n${setup.stderr}`)
95
+ const dumpMounted = dsh(['--profile', 'smoke', '--dump-config'])
96
+ if (dumpMounted.status !== 0 || dumpMounted.error) fail('dsh --dump-config failed on the setup-mounted profile', `${dumpMounted.stdout}\n${dumpMounted.stderr}`)
97
+ if (!dumpMounted.stdout.includes(entryId) || !/compaction-basic\n( name: .*\n)? disabled: true/.test(dumpMounted.stdout)) {
98
+ fail('the setup-mounted tree does not carry dsh-dcp with compaction-basic disabled', dumpMounted.stdout)
99
+ }
100
+ const remove = spawnSync(process.execPath, [path.join(repoRoot, 'scripts', 'setup.mjs'), '--remove', patchFile], { encoding: 'utf8' })
101
+ if (remove.status !== 0 || remove.error) fail('setup.mjs --remove failed', `${remove.stdout}\n${remove.stderr}`)
102
+ const dumpAfter = dsh(['--profile', 'smoke', '--dump-config'])
103
+ if (dumpAfter.status !== 0 || dumpAfter.error) fail('dsh --dump-config failed after removal', `${dumpAfter.stdout}\n${dumpAfter.stderr}`)
104
+ if (dumpAfter.stdout.includes(entryId)) {
105
+ fail(`the composed tree still contains the "${entryId}" entry after removal`, dumpAfter.stdout)
106
+ }
107
+ if (/compaction-basic\n( name: .*\n)? disabled: true/.test(dumpAfter.stdout)) {
108
+ fail('compaction-basic is still disabled after removal — the stock backend did not come back', dumpAfter.stdout)
109
+ }
110
+ if (!dumpAfter.stdout.includes('compaction-basic')) {
111
+ fail('compaction-basic vanished from the composed tree after removal', dumpAfter.stdout)
112
+ }
113
+ console.log('smoke-boot: setup.mjs mount → remove lifecycle restored the stock tree')
114
+
115
+ // Phase B — mount proof: the composed tree must carry the dsh-dcp entry, with
116
+ // compaction-basic disabled rather than removed-by-default. The hand-written
117
+ // mount points INTO the installed tarball (the true user shape), so the boot
118
+ // below exercises the packed artifact.
119
+ writeFileSync(path.join(profile, 'cordis.patch.yml'), `# scratch smoke profile: dsh-dcp mounted the setup.mjs way
120
+ - id: compaction-basic
121
+ name: '@deepseek-ai/dsh-compaction-basic'
122
+ disabled: true
123
+ - insert:
124
+ - id: ${entryId}
125
+ name: ${path.join(profile, 'node_modules', '@aiwayds', 'dsh-dcp', 'lib', 'index.js')}
126
+ config:
127
+ thresholdRatio: 0.7
128
+ language: zh
129
+ `)
95
130
  const dump = dsh(['--profile', 'smoke', '--dump-config'])
96
131
  if (dump.status !== 0 || dump.error) fail('dsh --dump-config failed on the scratch profile', `${dump.stdout}\n${dump.stderr}`)
97
132
  if (!dump.stdout.includes(entryId)) {
@@ -118,5 +153,5 @@ if (boot.signal !== 'SIGKILL' && boot.status !== 0) {
118
153
  fail(`dsh exited early with code ${boot.status} and no loader error — unexpected`, output)
119
154
  }
120
155
 
121
- console.log(`smoke-boot: PASS — ${ownName} mounted via cordis.patch.yml (compaction-basic disabled), composed into the scratch profile tree, and booted clean in real dsh (${boot.signal === 'SIGKILL' ? `survived ${bootSeconds}s boot window` : `exited ${boot.status}`})`)
156
+ console.log(`smoke-boot: PASS — ${ownName} mounted via cordis.patch.yml (compaction-basic disabled), composed into the scratch profile tree, booted clean in real dsh (${boot.signal === 'SIGKILL' ? `survived ${bootSeconds}s boot window` : `exited ${boot.status}`}); setup mount → remove lifecycle restored the stock tree`)
122
157
  rmSync(work, { recursive: true, force: true })