@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 +4 -0
- package/package.json +2 -2
- package/scripts/verify-dsh-compat.mjs +145 -0
- package/scripts/verify-package.mjs +136 -0
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.
|
|
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')
|