@godv61/dsh-task-engine 0.24.0 → 0.26.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/.acceptance.mjs +101 -39
- package/.assessment-batch1.mjs +32 -12
- package/.e2e-presets.mjs +40 -5
- package/.enforce-test.mjs +139 -0
- package/.evidence-test.mjs +118 -0
- package/.filter-test.mjs +93 -0
- package/.freeze-test.mjs +110 -1
- package/.hook-test.mjs +195 -190
- package/.p0-test.mjs +130 -46
- package/.preset-test.mjs +160 -160
- package/.revision-test.mjs +32 -3
- package/.roundtrip-test.mjs +178 -0
- package/.workflow-test.mjs +117 -4
- package/README.md +10 -8
- package/cordis.patch.yml +10 -10
- package/defaults/eng.json +70 -22
- package/docs/BRIEF-FOR-REVIEW.md +162 -162
- package/docs/CHANGELOG.md +49 -0
- package/docs/README.md +40 -40
- package/docs/configuration.md +14 -11
- package/docs/development.md +1 -1
- package/docs/faq.md +4 -4
- package/docs/listing/godv61__dsh-task-engine.yml +5 -5
- package/docs/listing/submission.md +83 -83
- package/docs/manual.html +31 -26
- package/docs/releases/0.23.2.md +21 -21
- package/docs/roadmap.md +34 -34
- package/docs/testing/0.23.2/R02/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +56 -56
- package/docs/testing/0.23.2/R03/344/270/232/345/212/241/346/265/213/350/257/225/346/230/216/347/273/206.md +38 -38
- package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +65 -65
- package/hooks/commit-msg +159 -72
- package/lib/client.js +963 -571
- package/lib/client.js.map +3 -3
- package/lib/controller.d.ts +11 -2
- package/lib/controller.js +33 -5
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +170 -28
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +105 -0
- package/lib/engine.js +93 -5
- package/lib/engine.js.map +1 -1
- package/lib/hook.js +17 -4
- package/lib/hook.js.map +1 -1
- package/lib/skill-audit.d.ts +14 -3
- package/lib/skill-audit.js +66 -3
- package/lib/skill-audit.js.map +1 -1
- package/lib/workflows.d.ts +78 -12
- package/lib/workflows.js +258 -94
- package/lib/workflows.js.map +1 -1
- package/package.json +9 -5
- package/preset/agent.cordis.yml +21 -21
- package/preset/enable.mjs +87 -87
- package/preset/persona.md +4 -4
- package/preset/preset.yml +1 -1
- package/rules/coding-conventions.md +6 -6
- package/rules/commit-conventions.md +6 -6
- package/rules/security-redlines.md +5 -5
- package/scripts/verify-dsh-compat.mjs +144 -144
- package/scripts/verify-package.mjs +1 -1
- package/skills/code-verify/SKILL.md +3 -3
package/preset/enable.mjs
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* One-shot activation helper: copy the shipped `standard` preset to a `eng`
|
|
4
|
-
* preset under `$DSH_HOME/.agent-presets`, swap its persona for the engineering
|
|
5
|
-
* persona, and append the `@godv61/dsh-task-engine/agent` row.
|
|
6
|
-
*
|
|
7
|
-
* Run after `dsh plugin --profile <name> add @godv61/dsh-task-engine`:
|
|
8
|
-
* node <this-file>
|
|
9
|
-
*
|
|
10
|
-
* The script writes files only under `$DSH_HOME/.agent-presets/eng`; it never
|
|
11
|
-
* touches the shipped preset install.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { createRequire } from 'node:module'
|
|
15
|
-
import { cp, mkdir, readFile, writeFile, stat } from 'node:fs/promises'
|
|
16
|
-
import { homedir } from 'node:os'
|
|
17
|
-
import { join } from 'node:path'
|
|
18
|
-
import { fileURLToPath } from 'node:url'
|
|
19
|
-
|
|
20
|
-
const require = createRequire(import.meta.url)
|
|
21
|
-
|
|
22
|
-
/** The persona block `standard` ships, in its exact indentation. */
|
|
23
|
-
const SHIPPED_PERSONA_BLOCK =
|
|
24
|
-
' text: >-\n'
|
|
25
|
-
+ ' You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.\n'
|
|
26
|
-
|
|
27
|
-
const AGENT_ROW = "\n- id: task-engine-agent\n name: '@godv61/dsh-task-engine/agent'\n"
|
|
28
|
-
|
|
29
|
-
async function exists(path) {
|
|
30
|
-
try {
|
|
31
|
-
await stat(path)
|
|
32
|
-
return true
|
|
33
|
-
} catch {
|
|
34
|
-
return false
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function main() {
|
|
39
|
-
let shippedRoot
|
|
40
|
-
try {
|
|
41
|
-
;({ SHIPPED_PRESET_ROOT: shippedRoot } = require('@deepseek-ai/dsh-agent-presets'))
|
|
42
|
-
} catch {
|
|
43
|
-
console.error('未找到 @deepseek-ai/dsh-agent-presets —— 请先执行 dsh plugin --profile <name> add @godv61/dsh-task-engine')
|
|
44
|
-
process.exitCode = 1
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
49
|
-
const source = join(shippedRoot, 'standard')
|
|
50
|
-
const target = join(home, '.agent-presets', 'eng')
|
|
51
|
-
|
|
52
|
-
if (await exists(target)) {
|
|
53
|
-
console.error(`预设 "eng" 已存在(${target}),本脚本不会覆盖;请先删除它再重跑,或改用它已有的 agent.cordis.yml。`)
|
|
54
|
-
process.exitCode = 1
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
await mkdir(join(home, '.agent-presets'), { recursive: true })
|
|
59
|
-
await cp(source, target, { recursive: true })
|
|
60
|
-
|
|
61
|
-
const compPath = join(target, 'agent.cordis.yml')
|
|
62
|
-
let comp = await readFile(compPath, 'utf8')
|
|
63
|
-
|
|
64
|
-
const persona = (await readFile(fileURLToPath(new URL('./persona.md', import.meta.url)), 'utf8')).trimEnd()
|
|
65
|
-
if (comp.includes(SHIPPED_PERSONA_BLOCK)) {
|
|
66
|
-
const block = ' text: |-\n' + persona.split('\n').map(line => ' ' + line).join('\n') + '\n'
|
|
67
|
-
comp = comp.replace(SHIPPED_PERSONA_BLOCK, block)
|
|
68
|
-
} else {
|
|
69
|
-
console.warn('未能定位 standard 的 persona 段,跳过 persona 替换(仍会追加 agent 行)。')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!comp.includes("name: '@godv61/dsh-task-engine/agent'")) {
|
|
73
|
-
comp = comp.trimEnd() + '\n' + AGENT_ROW
|
|
74
|
-
}
|
|
75
|
-
await writeFile(compPath, comp, 'utf8')
|
|
76
|
-
|
|
77
|
-
await writeFile(
|
|
78
|
-
join(target, 'preset.yml'),
|
|
79
|
-
'name: 工程化开发引擎\n'
|
|
80
|
-
+ 'description: 通过 dev_task 工具硬性持有需求评审→设计→开发→交付→代码审核的状态机,按项目 .dsh/eng.json 配置流转与提交门禁。\n',
|
|
81
|
-
'utf8',
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
console.log('完成。已创建预设 "eng":persona 已替换为工程人设,并追加了 @godv61/dsh-task-engine/agent 行。')
|
|
85
|
-
console.log('新建会话时选择「工程化开发引擎」即可激活流程。')
|
|
86
|
-
}
|
|
87
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-shot activation helper: copy the shipped `standard` preset to a `eng`
|
|
4
|
+
* preset under `$DSH_HOME/.agent-presets`, swap its persona for the engineering
|
|
5
|
+
* persona, and append the `@godv61/dsh-task-engine/agent` row.
|
|
6
|
+
*
|
|
7
|
+
* Run after `dsh plugin --profile <name> add @godv61/dsh-task-engine`:
|
|
8
|
+
* node <this-file>
|
|
9
|
+
*
|
|
10
|
+
* The script writes files only under `$DSH_HOME/.agent-presets/eng`; it never
|
|
11
|
+
* touches the shipped preset install.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createRequire } from 'node:module'
|
|
15
|
+
import { cp, mkdir, readFile, writeFile, stat } from 'node:fs/promises'
|
|
16
|
+
import { homedir } from 'node:os'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url)
|
|
21
|
+
|
|
22
|
+
/** The persona block `standard` ships, in its exact indentation. */
|
|
23
|
+
const SHIPPED_PERSONA_BLOCK =
|
|
24
|
+
' text: >-\n'
|
|
25
|
+
+ ' You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.\n'
|
|
26
|
+
|
|
27
|
+
const AGENT_ROW = "\n- id: task-engine-agent\n name: '@godv61/dsh-task-engine/agent'\n"
|
|
28
|
+
|
|
29
|
+
async function exists(path) {
|
|
30
|
+
try {
|
|
31
|
+
await stat(path)
|
|
32
|
+
return true
|
|
33
|
+
} catch {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
let shippedRoot
|
|
40
|
+
try {
|
|
41
|
+
;({ SHIPPED_PRESET_ROOT: shippedRoot } = require('@deepseek-ai/dsh-agent-presets'))
|
|
42
|
+
} catch {
|
|
43
|
+
console.error('未找到 @deepseek-ai/dsh-agent-presets —— 请先执行 dsh plugin --profile <name> add @godv61/dsh-task-engine')
|
|
44
|
+
process.exitCode = 1
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
49
|
+
const source = join(shippedRoot, 'standard')
|
|
50
|
+
const target = join(home, '.agent-presets', 'eng')
|
|
51
|
+
|
|
52
|
+
if (await exists(target)) {
|
|
53
|
+
console.error(`预设 "eng" 已存在(${target}),本脚本不会覆盖;请先删除它再重跑,或改用它已有的 agent.cordis.yml。`)
|
|
54
|
+
process.exitCode = 1
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await mkdir(join(home, '.agent-presets'), { recursive: true })
|
|
59
|
+
await cp(source, target, { recursive: true })
|
|
60
|
+
|
|
61
|
+
const compPath = join(target, 'agent.cordis.yml')
|
|
62
|
+
let comp = await readFile(compPath, 'utf8')
|
|
63
|
+
|
|
64
|
+
const persona = (await readFile(fileURLToPath(new URL('./persona.md', import.meta.url)), 'utf8')).trimEnd()
|
|
65
|
+
if (comp.includes(SHIPPED_PERSONA_BLOCK)) {
|
|
66
|
+
const block = ' text: |-\n' + persona.split('\n').map(line => ' ' + line).join('\n') + '\n'
|
|
67
|
+
comp = comp.replace(SHIPPED_PERSONA_BLOCK, block)
|
|
68
|
+
} else {
|
|
69
|
+
console.warn('未能定位 standard 的 persona 段,跳过 persona 替换(仍会追加 agent 行)。')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!comp.includes("name: '@godv61/dsh-task-engine/agent'")) {
|
|
73
|
+
comp = comp.trimEnd() + '\n' + AGENT_ROW
|
|
74
|
+
}
|
|
75
|
+
await writeFile(compPath, comp, 'utf8')
|
|
76
|
+
|
|
77
|
+
await writeFile(
|
|
78
|
+
join(target, 'preset.yml'),
|
|
79
|
+
'name: 工程化开发引擎\n'
|
|
80
|
+
+ 'description: 通过 dev_task 工具硬性持有需求评审→设计→开发→交付→代码审核的状态机,按项目 .dsh/eng.json 配置流转与提交门禁。\n',
|
|
81
|
+
'utf8',
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
console.log('完成。已创建预设 "eng":persona 已替换为工程人设,并追加了 @godv61/dsh-task-engine/agent 行。')
|
|
85
|
+
console.log('新建会话时选择「工程化开发引擎」即可激活流程。')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
88
|
await main()
|
package/preset/persona.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
You are an engineering-delivery coding agent, powered by the {{model}} model, running on DeepSeek Harness. Your working directory is {{cwd}}.
|
|
2
|
-
|
|
3
|
-
本仓库采用工程化交付流程,由 `dev_task` 工具硬性持有任务状态机:需求评审 → 设计 → 开发 → 交付 → 代码审核。阶段、流转门槛、提交格式都来自项目根 `.dsh/eng.json`(没有该文件则用内置默认)。任何开发类请求开始前,先加载 `eng-delivery` 技能。
|
|
4
|
-
|
|
1
|
+
You are an engineering-delivery coding agent, powered by the {{model}} model, running on DeepSeek Harness. Your working directory is {{cwd}}.
|
|
2
|
+
|
|
3
|
+
本仓库采用工程化交付流程,由 `dev_task` 工具硬性持有任务状态机:需求评审 → 设计 → 开发 → 交付 → 代码审核。阶段、流转门槛、提交格式都来自项目根 `.dsh/eng.json`(没有该文件则用内置默认)。任何开发类请求开始前,先加载 `eng-delivery` 技能。
|
|
4
|
+
|
|
5
5
|
铁律:一切阶段流转、确认、验证、评审和提交都必须通过 `dev_task`——它拒绝时是硬事实,必须修正前置条件,禁止绕过。只做任务范围内的本地提交;绝不 push、合并、创建 PR、执行数据库、操作 Jenkins、部署或发布。
|
package/preset/preset.yml
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
name: 工程化开发引擎
|
|
1
|
+
name: 工程化开发引擎
|
|
2
2
|
description: 通过 dev_task 工具硬性持有需求评审→设计→开发→交付→代码审核的状态机,按项目 .dsh/eng.json 配置流转与提交门禁。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# 通用编码规范
|
|
2
|
-
|
|
3
|
-
- 复用相邻实现与现有代码;单一调用方不为形式统一新增抽象层、包装层或额外分层。
|
|
4
|
-
- 禁止空 catch、printStackTrace、调试输出;吞错必须写清原因。
|
|
5
|
-
- 禁止无条件全表更新、删除或无边界查询。
|
|
6
|
-
- 命名清晰表达意图;不引入超过当前技术基线的语法、框架或 API。
|
|
1
|
+
# 通用编码规范
|
|
2
|
+
|
|
3
|
+
- 复用相邻实现与现有代码;单一调用方不为形式统一新增抽象层、包装层或额外分层。
|
|
4
|
+
- 禁止空 catch、printStackTrace、调试输出;吞错必须写清原因。
|
|
5
|
+
- 禁止无条件全表更新、删除或无边界查询。
|
|
6
|
+
- 命名清晰表达意图;不引入超过当前技术基线的语法、框架或 API。
|
|
7
7
|
- 不覆盖已有文件、用户改动或生成目标;不顺带重构无关历史。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# 提交规范
|
|
2
|
-
|
|
3
|
-
- 内置流程的提交消息使用 `【<task_id>】【TASK】说明` 或 `【<task_id>】【Tn】说明`,第一段必须是当前任务 id,不能填模块名;第二段使用 `dev_task status` 返回的 `commit.label`。自定义格式以任务冻结流程中的 `commit.message_hint` 和 `message_pattern` 为准。说明写结果,不写"修改代码"这类空泛动作。
|
|
4
|
-
- 只提交任务 files 范围内的文件;逐文件暂存,禁止 `git add .` / `git add -A`。
|
|
5
|
-
- 提交前 `git diff --cached --check`;发现范围外文件、密钥或环境配置时停止。
|
|
6
|
-
- 不 push、不合并、不建 PR、不部署、不发布。
|
|
1
|
+
# 提交规范
|
|
2
|
+
|
|
3
|
+
- 内置流程的提交消息使用 `【<task_id>】【TASK】说明` 或 `【<task_id>】【Tn】说明`,第一段必须是当前任务 id,不能填模块名;第二段使用 `dev_task status` 返回的 `commit.label`。自定义格式以任务冻结流程中的 `commit.message_hint` 和 `message_pattern` 为准。说明写结果,不写"修改代码"这类空泛动作。
|
|
4
|
+
- 只提交任务 files 范围内的文件;逐文件暂存,禁止 `git add .` / `git add -A`。
|
|
5
|
+
- 提交前 `git diff --cached --check`;发现范围外文件、密钥或环境配置时停止。
|
|
6
|
+
- 不 push、不合并、不建 PR、不部署、不发布。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# 安全红线
|
|
2
|
-
|
|
3
|
-
- 禁止读取、展示或修改密码、Token、私钥和生产凭据。
|
|
4
|
-
- 禁止把敏感请求体、文件正文或凭据写入日志。
|
|
5
|
-
- 禁止用户输入直接拼接 SQL、表名、列名、排序、路径或命令。
|
|
1
|
+
# 安全红线
|
|
2
|
+
|
|
3
|
+
- 禁止读取、展示或修改密码、Token、私钥和生产凭据。
|
|
4
|
+
- 禁止把敏感请求体、文件正文或凭据写入日志。
|
|
5
|
+
- 禁止用户输入直接拼接 SQL、表名、列名、排序、路径或命令。
|
|
6
6
|
- 权限与关键业务校验必须在服务端执行,前端校验不是安全边界。
|
|
@@ -1,145 +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
|
-
}
|
|
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
145
|
console.log('\nDSH compatibility verified')
|
|
@@ -42,7 +42,7 @@ const listing = execSync(`tar -tzf "${tgzPath}"`, { encoding: 'utf8' })
|
|
|
42
42
|
check('extract', existsSync(join(pkgDir, 'package.json')))
|
|
43
43
|
|
|
44
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', '
|
|
45
|
+
for (const required of ['hooks/commit-msg', 'lib/index.js', 'lib/client.js', 'lib/client.d.ts', 'preset/enable.mjs', 'README.md', '.p0-test.mjs', '.acceptance.mjs', '.hook-consistency.mjs', '.resource-test.mjs', '.workflow-test.mjs', '.hook-test.mjs', '.preset-test.mjs', '.assessment-batch1.mjs', '.e2e-presets.mjs', '.revision-test.mjs', '.freeze-test.mjs', '.enforce-test.mjs', '.evidence-test.mjs', '.roundtrip-test.mjs', '.filter-test.mjs']) {
|
|
46
46
|
check(`tarball contains ${required}`, listing.includes(required))
|
|
47
47
|
}
|
|
48
48
|
check('tarball excludes src sources', !listing.some(line => line.startsWith('src/')))
|
|
@@ -11,9 +11,9 @@ description: 交付验证节点:执行目标测试、编译与契约检查,
|
|
|
11
11
|
4. 文件或范围变更会使旧验证回执失效,审核修复后可在当前阶段重跑 verify。沙箱拒绝属于环境阻塞,不能改跑不相关的简单命令来冒充原验收通过。
|
|
12
12
|
5. 环境受阻时如实记"未编译/未联调";high_risk 核心行为无法验证时不得通过。
|
|
13
13
|
|
|
14
|
-
权限或沙箱拒绝后,不得切换等价命令、修改 ACL 或安全策略来规避限制。确认错误来自宿主权限后使用 Harness 正式审批;审批不可用时报告失败路径和所需授权,等待处理,不把工具访问限制推断为服务不可达。
|
|
15
|
-
|
|
16
|
-
`verify` / `skill_result` 的验证命令因沙箱受阻时,保持原命令,增加 `sandbox_permissions: "danger-full-access"` 和具体 `justification` 申请单次重试。批准发生在命令执行之前,只对本次调用生效;回执中的实际模式与退出码才是本次结果,不能引用之前 shell 调用通过的结果替代。保存完整测试输出后从已有日志提取用例,不为截取不同摘要重复跑已通过测试。
|
|
14
|
+
权限或沙箱拒绝后,不得切换等价命令、修改 ACL 或安全策略来规避限制。确认错误来自宿主权限后使用 Harness 正式审批;审批不可用时报告失败路径和所需授权,等待处理,不把工具访问限制推断为服务不可达。
|
|
15
|
+
|
|
16
|
+
`verify` / `skill_result` 的验证命令因沙箱受阻时,保持原命令,增加 `sandbox_permissions: "danger-full-access"` 和具体 `justification` 申请单次重试。批准发生在命令执行之前,只对本次调用生效;回执中的实际模式与退出码才是本次结果,不能引用之前 shell 调用通过的结果替代。保存完整测试输出后从已有日志提取用例,不为截取不同摘要重复跑已通过测试。
|
|
17
17
|
|
|
18
18
|
验证失败不得 advance。
|
|
19
19
|
|