@aiwayds/dsh-dcp 0.5.1 → 0.7.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
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
  ## 为什么做
package/lib/config.js CHANGED
@@ -60,11 +60,11 @@ export function splitConfig(config = {}) {
60
60
  for (const key of Object.keys(config)) {
61
61
  if (!known.has(key)) throw new Error(`DcpConfig: unknown key "${key}"`)
62
62
  }
63
- const basic = {}
63
+ const basic = /** @type {Record<string, unknown>} */ ({})
64
64
  for (const key of BASIC_CONFIG_KEYS) {
65
65
  if (config[key] !== undefined) basic[key] = config[key]
66
66
  }
67
- const dcp = {}
67
+ const dcp = /** @type {Record<string, unknown>} */ ({})
68
68
  for (const key of DCP_CONFIG_KEYS) {
69
69
  if (config[key] !== undefined) dcp[key] = config[key]
70
70
  }
@@ -75,7 +75,7 @@ export function splitConfig(config = {}) {
75
75
  * Validate and resolve dcp defaults.
76
76
  *
77
77
  * @param {Record<string, unknown>} raw - the dcp half of {@link splitConfig}.
78
- * @returns {Readonly<{dedup: boolean, purgeErrors: boolean, maxItems: number, maxItemChars: number, maxSummaryTokens: number, language: 'en'|'zh', tokenEstimate: 'cjk'|'ascii', protectedTools: readonly string[]}>}
78
+ * @returns {Readonly<{dedup: boolean, purgeErrors: boolean, maxItems: number, maxItemChars: number, maxSummaryTokens: number, language: 'en'|'zh', tokenEstimate: 'cjk'|'ascii', protectedTools: readonly string[], roundInterval: number, notice: boolean}>}
79
79
  */
80
80
  export function resolveDcpConfig(raw = {}) {
81
81
  if (raw.dedup !== undefined && typeof raw.dedup !== 'boolean') {
package/lib/index.js CHANGED
@@ -50,6 +50,11 @@ const { version: VERSION } = require('../package.json')
50
50
  * "Cannot read private member #triggerLabels from an object whose class did
51
51
  * not declare it", while symbol lookups traverse those receivers to this
52
52
  * instance's own state.
53
+ *
54
+ * Verified against dsh 0.1.2-alpha.3 (cordis 4.0.2): `withProps` /
55
+ * `createTraceable` / `applyTraceable` receivers and the `ctx.mixin` service
56
+ * forwarding are unchanged, and the base engine still brands no `#private`
57
+ * state — the symbol-keyed approach remains both necessary and valid.
53
58
  */
54
59
  const kRounds = Symbol('dsh-dcp.rounds')
55
60
  const kTriggerLabels = Symbol('dsh-dcp.triggerLabels')
@@ -111,6 +116,7 @@ export class DcpEngine extends BasicCompactionEngine {
111
116
  dcp
112
117
 
113
118
  /** Compaction counters surfaced by `/dcp`. */
119
+ /** @type {{compactions: number, shadowedTokens: number, lastAt: number|null}} */
114
120
  dcpStats
115
121
 
116
122
  /** Absolute module path, echoed by `/dcp set` for persistence snippets. */
@@ -230,6 +236,10 @@ export class DcpEngine extends BasicCompactionEngine {
230
236
  * round-interval trigger — the parent's `compactNow` bypasses
231
237
  * `compactRegion` (it drives `compactSurfaceRegion` directly), so without
232
238
  * this override the manual path would miss stats and the transcript notice.
239
+ *
240
+ * alpha.3 note: the base dereferences `signal` unguarded
241
+ * (`signal.throwIfAborted()`), so the signal is required — both this
242
+ * plugin's round trigger and the command invocation always pass one.
233
243
  */
234
244
  async compactNow(agent, signal, sourceCommandId) {
235
245
  const session = agent.session
package/lib/setup.js CHANGED
@@ -68,7 +68,7 @@ export function findBundledProfiles(profilesDir, pkg) {
68
68
  *
69
69
  * @param {string|undefined} text - existing file content, or undefined when the file is absent.
70
70
  * @param {{ name: string }} options - the resolvable dsh-dcp entry specifier to mount.
71
- * @returns {{ action: 'create'|'patch'|'skip', block?: string, note?: string }}
71
+ * @returns {{ action: 'create'|'patch', block: string, note?: string } | { action: 'skip' }}
72
72
  */
73
73
  export function planPatch(text, { name }) {
74
74
  if (text === undefined || text === null) {
package/lib/summarizer.js CHANGED
@@ -105,7 +105,7 @@ export function estimateTextTokens(text, mode = 'cjk') {
105
105
  * tool-result payloads (the same content the host meter prices, minus its
106
106
  * per-block overhead).
107
107
  *
108
- * @param {{ content?: Array<{ type: string }> }} message - any message-shaped object.
108
+ * @param {{ content?: Array<Record<string, any>> }} message - any message-shaped object.
109
109
  * @param {'cjk'|'ascii'} [mode] - pricing mode, see {@link estimateTextTokens}.
110
110
  * @returns {number} estimated tokens.
111
111
  */
@@ -275,7 +275,7 @@ function argDisplay(name, parsed) {
275
275
  * @returns {object} the extracted fact bundle.
276
276
  */
277
277
  export function extractFacts(messages, language = 'en') {
278
- const facts = {
278
+ const facts = /** @type {{messageCount: number, intents: string[], files: Map<string, {reads: number, writes: number}>, commands: string[], errors: string[], pendingTodos: string[], concepts: Set<string>, dupCounts: Map<string, {name: string, parsed: any, count: number}>, toolCallCount: number, lastUserText: string, lastAssistantText: string, carried: Map<string, string[]>}} */ ({
279
279
  messageCount: messages.length,
280
280
  intents: [],
281
281
  files: new Map(),
@@ -288,7 +288,7 @@ export function extractFacts(messages, language = 'en') {
288
288
  lastUserText: '',
289
289
  lastAssistantText: '',
290
290
  carried: new Map(),
291
- }
291
+ })
292
292
  const callsById = new Map()
293
293
  const errorRegexes = errorPatterns(language)
294
294
  const { checkbox, markers } = todoPatterns(language)
@@ -521,7 +521,7 @@ function composeTerse(facts, t, itemChars) {
521
521
  *
522
522
  * @param {{ messages: import('@deepseek-ai/dsh-llm').Message[] }} input - replayed region.
523
523
  * @param {object} dcp - resolved dcp config.
524
- * @param {(message: unknown) => number} [estimateMessageLike] - token estimator; defaults to {@link estimateMessageTokens}.
524
+ * @param {(message: any) => number} [estimateMessageLike] - token estimator; defaults to {@link estimateMessageTokens}.
525
525
  * @returns {{ summary: { type: 'text', text: string }[], provider: string, model: string }}
526
526
  */
527
527
  export function summarizeDeterministically(input, dcp, estimateMessageLike = (message) => estimateMessageTokens(message, dcp.tokenEstimate)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-dcp",
3
- "version": "0.5.1",
3
+ "version": "0.7.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",
@@ -24,7 +24,10 @@
24
24
  ],
25
25
  "scripts": {
26
26
  "test": "node --test",
27
- "setup": "node scripts/setup.mjs"
27
+ "setup": "node scripts/setup.mjs",
28
+ "link:closure": "node scripts/link-dsh-closure.mjs",
29
+ "check": "tsc --noEmit -p tsconfig.json",
30
+ "smoke": "node scripts/smoke-boot.mjs"
28
31
  },
29
32
  "keywords": [
30
33
  "dsh",
@@ -48,18 +51,18 @@
48
51
  },
49
52
  "dependencies": {},
50
53
  "peerDependencies": {
51
- "@deepseek-ai/cordis": "^4.0.1",
52
- "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
53
- "@deepseek-ai/dsh-brand": "0.1.0-rc.6",
54
- "@deepseek-ai/dsh-commands": "0.1.0-rc.6",
55
- "@deepseek-ai/dsh-compaction": "0.1.0-rc.6",
56
- "@deepseek-ai/dsh-compaction-basic": "0.1.0-rc.6",
57
- "@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.0-rc.6",
58
- "@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
59
- "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
60
- "@deepseek-ai/dsh-session": "0.1.0-rc.6",
61
- "@deepseek-ai/dsh-token-meter": "0.1.0-rc.6",
62
- "@deepseek-ai/schemastery": "^3.18.1"
54
+ "@deepseek-ai/cordis": "^4.0.2",
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
+ "@deepseek-ai/schemastery": "^3.18.2"
63
66
  },
64
67
  "peerDependenciesMeta": {
65
68
  "@deepseek-ai/cordis": {
@@ -100,33 +103,49 @@
100
103
  }
101
104
  },
102
105
  "devDependencies": {
103
- "@deepseek-ai/cordis": "^4.0.1",
104
- "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
105
- "@deepseek-ai/dsh-brand": "0.1.0-rc.6",
106
- "@deepseek-ai/dsh-commands": "0.1.0-rc.6",
107
- "@deepseek-ai/dsh-compaction": "0.1.0-rc.6",
108
- "@deepseek-ai/dsh-compaction-basic": "0.1.0-rc.6",
109
- "@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.0-rc.6",
110
- "@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
111
- "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
112
- "@deepseek-ai/dsh-session": "0.1.0-rc.6",
113
- "@deepseek-ai/dsh-token-meter": "0.1.0-rc.6",
114
- "@deepseek-ai/schemastery": "^3.18.1"
106
+ "@types/node": "^24.0.0",
107
+ "@deepseek-ai/cordis": "4.0.2",
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
+ "@deepseek-ai/schemastery": "3.18.2",
119
+ "typescript": "~5.9.0"
115
120
  },
116
121
  "overrides": {
117
- "@deepseek-ai/dsh-agent": "0.1.0-rc.6",
118
- "@deepseek-ai/dsh-brand": "0.1.0-rc.6",
119
- "@deepseek-ai/dsh-commands": "0.1.0-rc.6",
120
- "@deepseek-ai/dsh-compaction": "0.1.0-rc.6",
121
- "@deepseek-ai/dsh-compaction-tool-result-pruner": "0.1.0-rc.6",
122
- "@deepseek-ai/dsh-invariants": "0.1.0-rc.6",
123
- "@deepseek-ai/dsh-llm": "0.1.0-rc.6",
124
- "@deepseek-ai/dsh-session": "0.1.0-rc.6",
125
- "@deepseek-ai/dsh-token-meter": "0.1.0-rc.6"
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"
126
136
  },
127
137
  "dsh": {
128
138
  "bundle": {
129
139
  "patch": "./cordis.patch.yml"
130
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
+ }
131
150
  }
132
151
  }
@@ -175,7 +175,7 @@ function fmt(n) {
175
175
  */
176
176
  function simulate(events, mode, useDcp) {
177
177
  const surface = []
178
- const stats = { requests: 0, inputTokens: 0, hitTokens: 0, compactions: [] }
178
+ const stats = /** @type {{ requests: number, inputTokens: number, hitTokens: number, compactions: Array<{regionNodes: number, regionHost: number, regionCjk: number, summaryHost: number, summaryCjk: number, compression: string, totalAfter: number, preview: string}> }} */ ({ requests: 0, inputTokens: 0, hitTokens: 0, compactions: [] })
179
179
  let prevSurface = []
180
180
  let compactionId = 0
181
181
 
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Closure linker: point every `node_modules/@deepseek-ai/*` entry at one dsh
3
+ * closure (the installed dsh CLI's own `node_modules/@deepseek-ai` tree).
4
+ *
5
+ * Why this exists: dsh-dcp is a plugin that runs *inside* the installed dsh
6
+ * CLI, and its source imports the `@deepseek-ai/*` host packages (cordis,
7
+ * dsh-compaction-basic, dsh-llm, …). This repo is lib-as-source — there is no
8
+ * `src/` and no build step — so typecheck (`pnpm check`, tsc checkJs) and the
9
+ * node:test suite must resolve those imports against exactly ONE copy of the
10
+ * host graph: the closure the real runtime uses. Linking also guarantees a
11
+ * single `@deepseek-ai/cordis` instance, which the `declare module`
12
+ * augmentations (e.g. `ctx.commands`) require.
13
+ *
14
+ * NOTE on the guard below: unlike a TS repo this repo has no `src/` directory
15
+ * — `lib/` IS the source — so the "am I inside the repo?" check looks for
16
+ * `lib/` (plus `.git/`, which a published tarball never carries). The script
17
+ * is never wired as a lifecycle hook (no postinstall); it is run explicitly
18
+ * (CI step, `pnpm link:closure`), and the guard only protects stray manual
19
+ * runs from outside the repo.
20
+ *
21
+ * Resolution order for the closure:
22
+ * 0) `$DSH_CLOSURE_DIR` — explicit override, e.g. a scratch closure from
23
+ * `npm i --prefix ~/tmp/dsh-alpha-closure @deepseek-ai/dsh@alpha`:
24
+ * DSH_CLOSURE_DIR=~/tmp/dsh-alpha-closure/node_modules/@deepseek-ai node scripts/link-dsh-closure.mjs
25
+ * 1) the `dsh` bin on PATH → its package's node_modules/@deepseek-ai
26
+ * 2) `npm root -g` nested closure, then npm's flat global @deepseek-ai scope
27
+ *
28
+ * It is a no-op (exit 0) when no dsh closure is found.
29
+ */
30
+
31
+ import { execFileSync } from 'node:child_process'
32
+ import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
33
+ import { dirname, join } from 'node:path'
34
+ import { fileURLToPath } from 'node:url'
35
+
36
+ const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)))
37
+ // Guard: dev convenience for THIS repo only. lib/ is this repo's source (there
38
+ // is no src/), and .git/ only exists in a working clone — never in a packed
39
+ // tarball — so both together prove "inside the repo, not a consumer install".
40
+ if (!existsSync(join(repoRoot, 'lib')) || !existsSync(join(repoRoot, '.git'))) {
41
+ process.exit(0)
42
+ }
43
+ const scopeDir = join(repoRoot, 'node_modules', '@deepseek-ai')
44
+
45
+ /** The dsh closure: a node_modules dir whose @deepseek-ai scope is complete. */
46
+ function findDshClosure() {
47
+ // 0) Explicit override for dev/typecheck against an unreleased dsh line.
48
+ const override = process.env.DSH_CLOSURE_DIR
49
+ if (override !== undefined && override !== '') {
50
+ const dir = realpathSync(override)
51
+ if (existsSync(join(dir, 'cordis'))) return dir
52
+ console.warn(`[link-dsh-closure] DSH_CLOSURE_DIR=${override} lacks @deepseek-ai/cordis — ignoring override`)
53
+ }
54
+ // 1) Follow the `dsh` bin — the most faithful pointer to the installed CLI
55
+ // (`/opt/homebrew/bin/dsh` → …/lib/bin.js → pkg dir → its node_modules).
56
+ try {
57
+ const bin = execFileSync('which', ['dsh'], { encoding: 'utf8' }).trim()
58
+ if (bin !== '') {
59
+ const real = realpathSync(bin)
60
+ const closure = join(dirname(dirname(real)), 'node_modules', '@deepseek-ai')
61
+ if (existsSync(join(closure, 'cordis'))) return closure
62
+ }
63
+ } catch { /* dsh not on PATH */ }
64
+ // 2) Fall back to the global node_modules root — the dsh package's own
65
+ // nested closure (what current npm produces for a global install).
66
+ try {
67
+ const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8' }).trim()
68
+ const nested = join(root, '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai')
69
+ if (existsSync(join(nested, 'cordis'))) return nested
70
+ // 3) Last resort: npm's flat global layout, where dsh's @deepseek-ai/*
71
+ // deps are hoisted straight into <npm root -g>/@deepseek-ai next to
72
+ // the dsh package itself.
73
+ const flat = join(root, '@deepseek-ai')
74
+ if (existsSync(join(flat, 'cordis'))) return flat
75
+ } catch { /* npm unavailable */ }
76
+ return undefined
77
+ }
78
+
79
+ const closure = findDshClosure()
80
+ if (closure === undefined) {
81
+ console.warn('[link-dsh-closure] no dsh closure found — skipping @deepseek-ai links (dev without dsh)')
82
+ process.exit(0)
83
+ }
84
+
85
+ mkdirSync(scopeDir, { recursive: true })
86
+ let linked = 0
87
+ for (const name of readdirSync(closure)) {
88
+ const target = join(scopeDir, name)
89
+ const source = join(closure, name)
90
+ try {
91
+ // Replace any existing entry (stale symlink, or a local .pnpm copy a
92
+ // previous install created) with the closure link.
93
+ rmSync(target, { recursive: true, force: true })
94
+ symlinkSync(source, target, 'junction')
95
+ linked++
96
+ } catch (error) {
97
+ console.warn(`[link-dsh-closure] failed to link ${name}: ${(error instanceof Error ? error.message : String(error))}`)
98
+ }
99
+ }
100
+ console.log(`[link-dsh-closure] linked ${linked} @deepseek-ai/* packages from ${closure}`)
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ // Boot-smoke: mount the freshly packed dsh-dcp into a scratch dsh profile and
3
+ // boot it with the real dsh CLI.
4
+ //
5
+ // Unlike dsh-cron (which mounts through the bundle patch), this exercises the
6
+ // cordis.patch.yml mechanism — the same layer scripts/setup.mjs writes for
7
+ // users: the profile patch disables the default `compaction-basic` backend and
8
+ // inserts dsh-dcp by absolute path.
9
+ //
10
+ // 1. npm pack the repo → tarball
11
+ // 2. scratch $DSH_HOME/profiles/smoke: the dsh-base bundle plus the tarball
12
+ // as a file: dependency, and a cordis.patch.yml carrying the
13
+ // disable-compaction-basic + insert-dsh-dcp mount block
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)
20
+ //
21
+ // The dsh CLI comes from $DSH_BIN if set (e.g. a scratch alpha closure:
22
+ // DSH_BIN=~/tmp/dsh-alpha-closure/node_modules/@deepseek-ai/dsh/lib/bin.js),
23
+ // otherwise `dsh` on PATH.
24
+ //
25
+ // Exit 0 = mounted and boots clean. Temp dir is kept and printed on failure,
26
+ // removed on success.
27
+
28
+ import { spawnSync } from 'node:child_process'
29
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
30
+ import { readFile } from 'node:fs/promises'
31
+ import { tmpdir } from 'node:os'
32
+ import path from 'node:path'
33
+ import { fileURLToPath } from 'node:url'
34
+
35
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
36
+ const pkg = JSON.parse(await readFile(path.join(repoRoot, 'package.json'), 'utf8'))
37
+ const ownName = pkg.name // @aiwayds/dsh-dcp
38
+ const entryId = 'dsh-dcp'
39
+
40
+ const work = mkdtempSync(path.join(tmpdir(), 'dsh-dcp-smoke-'))
41
+ const home = path.join(work, 'dsh-home')
42
+ const profile = path.join(home, 'profiles', 'smoke')
43
+ mkdirSync(profile, { recursive: true })
44
+
45
+ function fail(message, output = '') {
46
+ console.error(`smoke-boot: FAIL — ${message}`)
47
+ if (output) console.error(output.split('\n').slice(0, 30).join('\n'))
48
+ console.error(`smoke-boot: scratch kept at ${work}`)
49
+ process.exit(1)
50
+ }
51
+
52
+ const dshBin = process.env.DSH_BIN || 'dsh'
53
+ const dsh = (args, opts = {}) => spawnSync(dshBin, args, { cwd: profile, encoding: 'utf8', env: { ...process.env, DSH_HOME: home }, ...opts })
54
+
55
+ const pack = spawnSync('npm', ['pack', '--pack-destination', work], { cwd: repoRoot, encoding: 'utf8' })
56
+ if (pack.status !== 0 || pack.error) fail('npm pack failed', `${pack.stdout}\n${pack.stderr}`)
57
+ const tarball = path.join(work, pack.stdout.trim().split('\n').at(-1) ?? '')
58
+
59
+ 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
+ `)
72
+ writeFileSync(path.join(profile, 'pnpm-workspace.yaml'), 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n')
73
+ writeFileSync(path.join(profile, 'package.json'), JSON.stringify({
74
+ name: 'dsh-profile-smoke',
75
+ private: true,
76
+ dependencies: {
77
+ [ownName]: `file:${tarball}`,
78
+ },
79
+ dsh: {
80
+ profile: {
81
+ // dsh-base brings the standard tree (including compaction-basic) that
82
+ // the patch layer then re-wires; dsh-dcp itself mounts via the patch.
83
+ bundles: [
84
+ '@deepseek-ai/dsh-base',
85
+ ],
86
+ },
87
+ },
88
+ }, null, 2) + '\n')
89
+
90
+ const install = spawnSync('pnpm', ['install'], { cwd: profile, encoding: 'utf8' })
91
+ if (install.status !== 0 || install.error) fail('pnpm install in the scratch profile failed', `${install.stdout}\n${install.stderr}`)
92
+
93
+ // Phase 1 — mount proof: the composed tree must carry the dsh-dcp entry, with
94
+ // compaction-basic disabled rather than removed-by-default.
95
+ const dump = dsh(['--profile', 'smoke', '--dump-config'])
96
+ if (dump.status !== 0 || dump.error) fail('dsh --dump-config failed on the scratch profile', `${dump.stdout}\n${dump.stderr}`)
97
+ if (!dump.stdout.includes(entryId)) {
98
+ fail(`the composed profile tree does not contain the "${entryId}" entry — the patch mount is broken`, dump.stdout)
99
+ }
100
+
101
+ // Phase 2 — boot proof: the plugin tree must LOAD without a loader error.
102
+ const bootSeconds = 25
103
+ const boot = dsh(['--profile', 'smoke'], { timeout: bootSeconds * 1000, killSignal: 'SIGKILL' })
104
+ const output = `${boot.stdout ?? ''}\n${boot.stderr ?? ''}`
105
+ const loaderErrors = [
106
+ /plugin tree failed to load/,
107
+ /failed to apply loader entry/,
108
+ /cannot get property ".*" without inject/,
109
+ /cannot get required service/,
110
+ /Cannot find (package|module)/,
111
+ /unknown key/,
112
+ ]
113
+ const hit = loaderErrors.filter((re) => re.test(output))
114
+ if (hit.length > 0) {
115
+ fail('the real host failed to load the plugin tree:', output.split('\n').filter((line) => hit.some((re) => re.test(line)) || /Error/.test(line)).slice(0, 15).join('\n'))
116
+ }
117
+ if (boot.signal !== 'SIGKILL' && boot.status !== 0) {
118
+ fail(`dsh exited early with code ${boot.status} and no loader error — unexpected`, output)
119
+ }
120
+
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}`})`)
122
+ rmSync(work, { recursive: true, force: true })