@godv61/dsh-task-engine 0.23.4 → 0.23.5

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/docs/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@
4
4
 
5
5
  按版本查阅功能变化。当前使用方式以[项目首页](../README.md)和使用指南为准;历史条目中的实现方式、限制与测试数量可能已被后续版本替代。
6
6
 
7
+ ## 0.23.5
8
+
9
+ - **修复 `scripts/` 未随包发布,导致两条已声明的 npm script 在安装后无法执行**。`package.json` 声明了 `verify:package` 与 `verify:dsh`,但 `files` 白名单不含 `scripts/**`,用户装包后运行它们会直接 `MODULE_NOT_FOUND`。该问题早于 0.23.3 存在(`verify:package` 一直如此),0.23.4 新增的 `verify:dsh` 只是沿用了同一模式。现在把 `scripts/**` 纳入白名单,并在 `verify:package` 中加断言:包内必须能找到每一条 `package.json` 里声明的 `node <file>` script 目标。
10
+
7
11
  ## 0.23.4
8
12
 
9
13
  - **关闭两处 Remote 路径边界漏洞**。`checkedPath` 此前用**原始字符串**比对禁止前缀,`D:/proj/../../Windows` 不匹配任何前缀,却被后续 `join()` 解析到 `C:/Windows`;实测 8 个越界样本中旧实现放过 7 个。现在先 `resolve()` 归一化再判断,禁止列表不再绑定盘符(`D:/Windows`、`E:/Program Files` 同样拒绝),并拒绝裸盘符根(`D:/`、`C:/`)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godv61/dsh-task-engine",
3
- "version": "0.23.4",
3
+ "version": "0.23.5",
4
4
  "description": "Preset-workflow engineering delivery engine for DeepSeek Harness: a `dev_task` tool that hard-gates stage transitions and commit scope/message, per-node skill/rule bindings with progressive disclosure, a workbench to pick a flow preset and mount skills/rules, and preset-scoped activation — the tool and skills activate only for sessions on a preset that names the agent row.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -33,6 +33,7 @@
33
33
  "skills/**",
34
34
  "rules/**",
35
35
  "preset/**",
36
+ "scripts/**",
36
37
  "docs/**",
37
38
  "README.md",
38
39
  ".p0-test.mjs",
@@ -83,7 +84,6 @@
83
84
  "@types/react": "~18.3.1",
84
85
  "esbuild": "^0.24.0",
85
86
  "react": "^18.2.0",
86
- "semver": "^7.6.0",
87
87
  "typescript": "^5.7.2",
88
88
  "zod": "^3.23.8"
89
89
  },
@@ -0,0 +1,145 @@
1
+ /**
2
+ * DSH codec-contract compatibility check.
3
+ *
4
+ * The plugin's Typert Remote contribution must satisfy whichever strict-codec
5
+ * contract the installed DSH expects. DSH 0.1.6-alpha.2 replaced the
6
+ * materialized `schema` field with a lazy `create: () => TypertSchema` factory
7
+ * and rejects the old shape at plugin load, which took the whole contribution
8
+ * down at boot — a failure the unit tests could not see, because they never
9
+ * load the descriptors through a real registry.
10
+ *
11
+ * npm semver cannot wildcard prereleases, so the peer range enumerates them and
12
+ * a new DSH alpha silently falls outside it; pnpm only warns at install time.
13
+ * This check runs against a real DSH checkout, where the contract actually
14
+ * lives, and fails loudly when the two drift apart again.
15
+ *
16
+ * Run: `node scripts/verify-dsh-compat.mjs [path-to-dsh-checkout]`
17
+ * @module dsh-task-engine/scripts/verify-dsh-compat
18
+ */
19
+
20
+ import { existsSync, readFileSync } from 'node:fs'
21
+ import { join } from 'node:path'
22
+ import { fileURLToPath } from 'node:url'
23
+
24
+ const DEFAULT_DSH_ROOT = 'D:/dsharness/deepseek-harness'
25
+
26
+ const problems = []
27
+ function check(name, ok, detail = '') {
28
+ console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail === '' ? '' : `: ${detail}`}`)
29
+ if (!ok) problems.push(name)
30
+ }
31
+
32
+ /** Locate the typert protocol package inside a DSH checkout. */
33
+ function protocolDir(root) {
34
+ const candidates = [
35
+ join(root, 'packages', 'typert', 'protocol'),
36
+ join(root, 'node_modules', '@deepseek-ai', 'dsh-typert-protocol'),
37
+ ]
38
+ return candidates.find(dir => existsSync(join(dir, 'package.json')))
39
+ }
40
+
41
+ const dshRoot = process.argv[2] ?? DEFAULT_DSH_ROOT
42
+ console.log(`DSH checkout: ${dshRoot}`)
43
+
44
+ if (!existsSync(dshRoot)) {
45
+ // Absent checkout is not a failure: this script is advisory outside CI.
46
+ console.log('ok (skipped) no DSH checkout at that path')
47
+ process.exit(0)
48
+ }
49
+
50
+ const dir = protocolDir(dshRoot)
51
+ if (dir === undefined) {
52
+ check('locate the typert protocol package', false, 'not found under the DSH checkout')
53
+ process.exit(1)
54
+ }
55
+ const protocol = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
56
+ console.log(` typert protocol version: ${protocol.version}`)
57
+
58
+ // 1. Which contract does this DSH expect? Read the type, not a guess.
59
+ const typesPath = [
60
+ join(dir, 'src', 'types.ts'),
61
+ join(dir, 'lib', 'types', 'types.d.ts'),
62
+ ].find(path => existsSync(path))
63
+ const typesText = typesPath === undefined ? '' : readFileSync(typesPath, 'utf8')
64
+ const wantsCreate = /readonly create: \(\) => TypertSchema/u.test(typesText)
65
+ const wantsSchema = /readonly schema: TypertSchema/u.test(typesText)
66
+ console.log(` contract: ${wantsCreate ? 'create() factory' : wantsSchema ? 'schema field' : 'unknown'}`)
67
+
68
+ // 2. Our descriptors must satisfy it. Import the built contribution and inspect
69
+ // every strict codec exactly as the DSH registry would.
70
+ const pluginRoot = fileURLToPath(new URL('..', import.meta.url))
71
+ const clientBundle = join(pluginRoot, 'lib', 'client.js')
72
+ if (!existsSync(clientBundle)) {
73
+ check('built client bundle exists', false, 'run `npm run build` first')
74
+ process.exit(1)
75
+ }
76
+ const bundleText = readFileSync(clientBundle, 'utf8')
77
+ const strictCodecs = (bundleText.match(/mode:\s*"strict"/gu) ?? []).length
78
+ const withCreate = (bundleText.match(/create:\s*\(\)\s*=>/gu) ?? []).length
79
+ const withSchema = (bundleText.match(/schema:/gu) ?? []).length
80
+
81
+ check('the bundle carries strict codecs', strictCodecs > 0, `${strictCodecs} found`)
82
+ if (wantsCreate) {
83
+ check('every strict codec provides create()', withCreate >= strictCodecs,
84
+ `${withCreate} of ${strictCodecs}`)
85
+ }
86
+ if (wantsSchema) {
87
+ check('every strict codec provides schema', withSchema >= strictCodecs,
88
+ `${withSchema} of ${strictCodecs}`)
89
+ }
90
+
91
+ // 3. The peer range must actually admit the installed DSH version, because npm
92
+ // semver will not match a prerelease the range does not name. Implemented
93
+ // here rather than via the `semver` package: that is a devDependency, so a
94
+ // consumer running this script from the installed tarball has no copy, and
95
+ // the whole point of this check is to run against whatever DSH is present.
96
+ const pkg = JSON.parse(readFileSync(join(pluginRoot, 'package.json'), 'utf8'))
97
+ const range = pkg.peerDependencies['@deepseek-ai/dsh-typert-protocol'] ?? ''
98
+ check(`peer range admits DSH ${protocol.version}`, rangeAdmits(range, protocol.version), range)
99
+
100
+ /**
101
+ * Whether one of a `||`-joined range's comparators admits `version`.
102
+ *
103
+ * npm semver matches a prerelease only when the comparator names a prerelease
104
+ * on the SAME major.minor.patch, which is why enumerating versions is the only
105
+ * way to cover a DSH alpha. This mirrors that rule for the caret and exact
106
+ * forms the manifest actually uses; it is deliberately conservative, and an
107
+ * unrecognized comparator reports `false` so the check fails loudly rather than
108
+ * passing by accident.
109
+ * @param range - the peer range expression.
110
+ * @param version - the installed DSH version to test.
111
+ * @returns true when the range admits the version.
112
+ */
113
+ function rangeAdmits(range, version) {
114
+ const parse = text => {
115
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/u.exec(text.trim())
116
+ return match === null
117
+ ? undefined
118
+ : { major: +match[1], minor: +match[2], patch: +match[3], pre: match[4] }
119
+ }
120
+ const target = parse(version)
121
+ if (target === undefined) return false
122
+ for (const part of range.split('||').map(text => text.trim()).filter(Boolean)) {
123
+ const caret = /^\^(.+)$/u.exec(part)
124
+ const lower = parse(caret === null ? part : caret[1])
125
+ if (lower === undefined) continue
126
+ // Same prerelease tuple, or a plain release on the same caret line.
127
+ const sameTuple = target.major === lower.major
128
+ && target.minor === lower.minor
129
+ && target.patch === lower.patch
130
+ if (lower.pre !== undefined) {
131
+ if (sameTuple) return true
132
+ } else if (target.pre === undefined && target.major === lower.major) {
133
+ if (caret === null ? sameTuple : target.minor >= lower.minor) return true
134
+ }
135
+ }
136
+ return false
137
+ }
138
+
139
+ if (problems.length > 0) {
140
+ console.error(`\n${problems.length} DSH compatibility problem(s)`)
141
+ console.error('A new DSH alpha likely changed the Typert contract. Update src/client/remote.ts')
142
+ console.error('and add the version to peerDependencies before releasing.')
143
+ process.exit(1)
144
+ }
145
+ console.log('\nDSH compatibility verified')
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Black-box verification of the npm tarball before publishing: packs the
3
+ * package, extracts it into a clean temp dir, and proves the published entry
4
+ * points actually work — main import, browser client registration, the bundled
5
+ * `.p0-test.mjs`, the CLI, and the `files` whitelist.
6
+ *
7
+ * Run: `npm run verify:package`
8
+ * @module dsh-task-engine/scripts/verify-package
9
+ */
10
+
11
+ import { execSync } from 'node:child_process'
12
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+ import { fileURLToPath, pathToFileURL } from 'node:url'
16
+
17
+ const pkgRoot = fileURLToPath(new URL('..', import.meta.url))
18
+ const failures = []
19
+ function check(name, ok, detail = '') {
20
+ console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail !== '' ? `: ${detail}` : ''}`)
21
+ if (!ok) failures.push(name)
22
+ }
23
+
24
+ // 1. pack
25
+ let tgzName
26
+ try {
27
+ const packed = JSON.parse(execSync('npm pack --json', { cwd: pkgRoot, encoding: 'utf8' }))
28
+ tgzName = packed[0]?.filename
29
+ } catch (error) {
30
+ check('npm pack', false, String(error))
31
+ process.exit(1)
32
+ }
33
+ check('npm pack', typeof tgzName === 'string', tgzName)
34
+
35
+ // 2. extract into a clean temp dir
36
+ const tmp = mkdtempSync(join(tmpdir(), 'dsh-pkg-verify-'))
37
+ const tgzPath = join(pkgRoot, tgzName)
38
+ execSync(`tar -xzf "${tgzPath}"`, { cwd: tmp, stdio: 'pipe' })
39
+ const pkgDir = join(tmp, 'package')
40
+ const listing = execSync(`tar -tzf "${tgzPath}"`, { encoding: 'utf8' })
41
+ .trim().split(/\r?\n/u).map(line => line.replace(/^package\//u, ''))
42
+ check('extract', existsSync(join(pkgDir, 'package.json')))
43
+
44
+ // 3. files whitelist sanity
45
+ for (const required of ['hooks/commit-msg', 'lib/index.js', 'lib/client.js', 'lib/client.d.ts', '.p0-test.mjs', 'preset/enable.mjs', 'README.md', '.resource-test.mjs', '.workflow-test.mjs', '.hook-test.mjs']) {
46
+ check(`tarball contains ${required}`, listing.includes(required))
47
+ }
48
+ check('tarball excludes src sources', !listing.some(line => line.startsWith('src/')))
49
+ check('tarball excludes build scripts', !listing.some(line => line.endsWith('build-client.mjs') || line.endsWith('build-hook.mjs')))
50
+
51
+ // Every `node <file>` script the manifest declares must actually ship, EXCEPT
52
+ // the build-time ones that `prepare` needs in the source tree only. A script
53
+ // whose target is absent from `files` installs fine and then dies with
54
+ // MODULE_NOT_FOUND — which is exactly what verify:package and verify:dsh did
55
+ // until this assertion existed, because neither scripts/ path was published.
56
+ {
57
+ const manifest = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'))
58
+ // Scripts that legitimately run only before publishing, never from a consumer.
59
+ const BUILD_ONLY = new Set(['prepare', 'build'])
60
+ const missing = []
61
+ for (const [name, command] of Object.entries(manifest.scripts ?? {})) {
62
+ if (BUILD_ONLY.has(name)) continue
63
+ for (const match of String(command).matchAll(/\bnode\s+(?:--[^\s]+\s+)*([^\s&|]+)/gu)) {
64
+ const target = match[1].replace(/^\.\//u, '')
65
+ if (!target.endsWith('.mjs') && !target.endsWith('.js')) continue
66
+ if (!listing.includes(target)) missing.push(`${name} -> ${target}`)
67
+ }
68
+ }
69
+ check('every declared node script ships in the tarball', missing.length === 0, missing.join(', '))
70
+ }
71
+
72
+ // 3b. install the tarball into a clean project so peer dependencies resolve,
73
+ // exactly like a real consumer's `npm install`.
74
+ const installDir = mkdtempSync(join(tmpdir(), 'dsh-pkg-install-'))
75
+ import { writeFileSync } from 'node:fs'
76
+ writeFileSync(join(installDir, 'package.json'), JSON.stringify({ name: 'probe', private: true, type: 'module' }))
77
+ try {
78
+ execSync(`npm install "${tgzPath}" --no-audit --no-fund`, { cwd: installDir, encoding: 'utf8', stdio: 'pipe' })
79
+ check('npm install tarball', true)
80
+ } catch (error) {
81
+ check('npm install tarball', false, String(error.stderr ?? error))
82
+ process.exit(1)
83
+ }
84
+ const installedDir = join(installDir, 'node_modules', '@godv61', 'dsh-task-engine')
85
+
86
+ // 4. main entry imports
87
+ try {
88
+ await import(pathToFileURL(join(installedDir, 'lib/index.js')).href)
89
+ check('import main entry', true)
90
+ } catch (error) {
91
+ check('import main entry', false, error instanceof Error ? error.message : String(error))
92
+ }
93
+
94
+ // 5. browser client registers through the DSH module loader
95
+ try {
96
+ const registry = { loaded: undefined }
97
+ globalThis.window = { __ModuleLoader__: { load(reg) { registry.loaded = reg } } }
98
+ await import(pathToFileURL(join(installedDir, 'lib/client.js')).href)
99
+ check('client bundle registers via ModuleLoader', registry.loaded?.id === '@godv61/dsh-task-engine')
100
+ } catch (error) {
101
+ check('client bundle registers via ModuleLoader', false, error instanceof Error ? error.message : String(error))
102
+ }
103
+
104
+ // 6. in-package acceptance test
105
+ try {
106
+ const out = execSync('node .p0-test.mjs', { cwd: installedDir, encoding: 'utf8', stdio: 'pipe' })
107
+ check('in-package .p0-test.mjs', /passed/u.test(out), out.trim().split('\n').at(-1))
108
+ } catch (error) {
109
+ check('in-package .p0-test.mjs', false, String(error.stderr ?? error))
110
+ }
111
+
112
+ // Resource package behavior from the installed artifact, including the commit
113
+ // hook running for real inside throwaway repositories.
114
+ try {
115
+ const out = execSync('node --test --test-reporter=tap .resource-test.mjs .workflow-test.mjs .hook-test.mjs', { cwd: installedDir, encoding: 'utf8', stdio: 'pipe' })
116
+ check('in-package resource, workflow and hook behavior', /# fail 0/u.test(out))
117
+ } catch (error) { check('in-package resource, workflow and hook behavior', false, String(error.stderr ?? error)) }
118
+
119
+ // 7. CLI parses
120
+ try {
121
+ const cli = join(installedDir, 'preset', 'enable.mjs')
122
+ execSync(`node --check "${cli}"`, { stdio: 'pipe' })
123
+ check('CLI enable.mjs parses', true)
124
+ } catch (error) {
125
+ check('CLI enable.mjs parses', false, String(error.stderr ?? error))
126
+ }
127
+
128
+ rmSync(tmp, { recursive: true, force: true })
129
+ rmSync(installDir, { recursive: true, force: true })
130
+ try { rmSync(tgzPath, { force: true }) } catch { /* keep the tarball when the FS declines */ }
131
+
132
+ if (failures.length > 0) {
133
+ console.error(`\n${failures.length} failure(s)`)
134
+ process.exit(1)
135
+ }
136
+ console.log('\npackage black-box verification passed')