@godv61/dsh-task-engine 0.27.1 → 0.29.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 +11 -11
- package/.adaptive-test.mjs +189 -0
- package/.assessment-batch1.mjs +14 -14
- package/.codex-project-test.mjs +80 -80
- package/.enforce-test.mjs +12 -12
- package/.evidence-test.mjs +8 -8
- package/.filter-test.mjs +6 -6
- package/.freeze-test.mjs +44 -44
- package/.hook-test.mjs +19 -2
- package/.p0-test.mjs +21 -21
- package/.preset-test.mjs +11 -1
- package/.revision-test.mjs +16 -16
- package/.roundtrip-test.mjs +247 -247
- package/.workflow-test.mjs +122 -120
- package/README.md +33 -27
- package/cordis.patch.yml +158 -9
- package/docs/CHANGELOG.md +44 -30
- package/docs/README.md +6 -5
- package/docs/adaptive-workflows.md +72 -0
- package/docs/configuration.md +25 -25
- package/docs/development.md +1 -1
- package/docs/faq.md +14 -6
- package/docs/getting-started.md +8 -5
- package/docs/manual-legacy.html +380 -0
- package/docs/manual.html +124 -378
- package/docs/resource-install.md +5 -5
- package/docs/roadmap.md +8 -9
- package/hooks/commit-msg +39 -22
- package/lib/adaptive.d.ts +15 -0
- package/lib/adaptive.js +54 -0
- package/lib/adaptive.js.map +1 -0
- package/lib/client.js +719 -433
- package/lib/client.js.map +3 -3
- package/lib/controller.d.ts +50 -0
- package/lib/controller.js +132 -0
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +413 -18
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +20 -0
- package/lib/engine.js +4 -1
- package/lib/engine.js.map +1 -1
- package/lib/hook.js +50 -29
- package/lib/hook.js.map +1 -1
- package/lib/project-init.d.ts +23 -0
- package/lib/project-init.js +94 -0
- package/lib/project-init.js.map +1 -0
- package/lib/skill-audit.js +8 -7
- package/lib/skill-audit.js.map +1 -1
- package/lib/sonar.d.ts +33 -0
- package/lib/sonar.js +80 -0
- package/lib/sonar.js.map +1 -0
- package/package.json +15 -16
- package/preset/agent.cordis.yml +3 -3
- package/preset/enable.mjs +2 -2
- package/preset/persona.md +4 -2
- package/scripts/verify-package.mjs +9 -5
- package/skills/architecture-design/SKILL.md +11 -0
- package/skills/code-development/SKILL.md +11 -0
- package/skills/code-review/SKILL.md +11 -0
- package/skills/eng-delivery/SKILL.md +19 -15
- package/skills/requirements-analysis/SKILL.md +11 -0
- package/skills/task-orchestration/SKILL.md +11 -0
- package/skills/test-validation/SKILL.md +11 -0
package/.acceptance.mjs
CHANGED
|
@@ -42,14 +42,14 @@ check('选流程不产生绑定', Object.keys(resolveFlow('standard', { flow: 's
|
|
|
42
42
|
|
|
43
43
|
console.log('')
|
|
44
44
|
console.log('=== 3. 采用后完全可编辑、可删除 ===')
|
|
45
|
-
for (const id of ['standard', 'agile', 'minimal']) {
|
|
46
|
-
const adopted = adoptRecommendation(id)
|
|
47
|
-
check(`${id} 采用后仍不绑定业务技能`, Object.keys(adopted.stage_bindings ?? {}).length === 0)
|
|
48
|
-
const retained = adoptRecommendation(id, { flow: id, stage_bindings: {
|
|
49
|
-
开发: { skill_refs: [{ source: 'project', name: 'mine' }] },
|
|
50
|
-
} })
|
|
51
|
-
check(`${id} 采用推荐文本时保留用户技能`, retained.stage_bindings.开发.skill_refs[0].name === 'mine')
|
|
52
|
-
if (id !== 'minimal') check(`${id} 推荐提交文本实际写入`, retained.commit.message_pattern !== '')
|
|
45
|
+
for (const id of ['standard', 'agile', 'minimal']) {
|
|
46
|
+
const adopted = adoptRecommendation(id)
|
|
47
|
+
check(`${id} 采用后仍不绑定业务技能`, Object.keys(adopted.stage_bindings ?? {}).length === 0)
|
|
48
|
+
const retained = adoptRecommendation(id, { flow: id, stage_bindings: {
|
|
49
|
+
开发: { skill_refs: [{ source: 'project', name: 'mine' }] },
|
|
50
|
+
} })
|
|
51
|
+
check(`${id} 采用推荐文本时保留用户技能`, retained.stage_bindings.开发.skill_refs[0].name === 'mine')
|
|
52
|
+
if (id !== 'minimal') check(`${id} 推荐提交文本实际写入`, retained.commit.message_pattern !== '')
|
|
53
53
|
const replaced = resolveFlow(id, { ...adopted, stage_bindings: {} }).config
|
|
54
54
|
check(`${id} 清空后为空(不补回)`, Object.keys(replaced.stage_bindings ?? {}).length === 0)
|
|
55
55
|
const mine = resolveFlow(id, {
|
|
@@ -106,13 +106,13 @@ for (const id of ['standard', 'agile', 'minimal']) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
console.log('')
|
|
109
|
-
console.log('=== 7. defaults/eng.json 是空绑定配置示例 ===')
|
|
109
|
+
console.log('=== 7. defaults/eng.json 是空绑定配置示例 ===')
|
|
110
110
|
const doc = JSON.parse(readFileSync('defaults/eng.json', 'utf8'))
|
|
111
111
|
const merged = resolveFlow(doc.flow, doc)
|
|
112
112
|
check('defaults/eng.json 可解析', merged.ok)
|
|
113
113
|
if (merged.ok) {
|
|
114
114
|
check('defaults/eng.json 无验证问题', validateWorkflow(merged.config).length === 0)
|
|
115
|
-
check('defaults/eng.json 不含技能', Object.keys(merged.config.stage_bindings ?? {}).length === 0)
|
|
115
|
+
check('defaults/eng.json 不含技能', Object.keys(merged.config.stage_bindings ?? {}).length === 0)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
console.log('')
|
|
@@ -153,4 +153,4 @@ check('三者范围互不相同', new Set([r.join(), s.join(), d.join()]).size =
|
|
|
153
153
|
|
|
154
154
|
console.log('')
|
|
155
155
|
console.log(failures === 0 ? '✅ 全部验收项通过' : `❌ ${failures} 项未通过`)
|
|
156
|
-
process.exit(failures === 0 ? 0 : 1)
|
|
156
|
+
process.exit(failures === 0 ? 0 : 1)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { adaptiveWorkflow, COMPLEXITY_OPTIONS } from './lib/adaptive.js'
|
|
7
|
+
import { validateWorkflow } from './lib/engine.js'
|
|
8
|
+
import { registerDevTask } from './lib/dev-task.js'
|
|
9
|
+
import { inspectSonar } from './lib/sonar.js'
|
|
10
|
+
import Controller from './lib/controller.js'
|
|
11
|
+
|
|
12
|
+
const home = mkdtempSync(join(tmpdir(), 'dsh-adaptive-'))
|
|
13
|
+
process.env.DSH_HOME = home
|
|
14
|
+
process.on('exit', () => rmSync(home, { recursive: true, force: true }))
|
|
15
|
+
|
|
16
|
+
function fixture(extra = {}) {
|
|
17
|
+
const cwd = resolve('adaptive-project')
|
|
18
|
+
const files = new Map([[join(cwd, 'package.json'), '{"name":"adaptive-project","dependencies":{"vue":"^2.7.0"}}'],
|
|
19
|
+
[join(cwd, '.git/HEAD'), 'ref: refs/heads/feature'] , ...Object.entries(extra).map(([p, content]) => [join(cwd, p), content])])
|
|
20
|
+
const events = []
|
|
21
|
+
const session = { id: 'adaptive', header: { cwd }, snapshotEvents: () => events }
|
|
22
|
+
const key = (path, options) => join(options?.cwd ?? cwd, path)
|
|
23
|
+
let execute
|
|
24
|
+
const fs = {
|
|
25
|
+
resolve: async (path, options) => ({ path: key(path, options) }),
|
|
26
|
+
readText: async target => files.get(target.path),
|
|
27
|
+
lstat: async (path, options) => files.has(key(path, options)) ? { version: 'v1' } : undefined,
|
|
28
|
+
writeText: async (target, content, intent) => {
|
|
29
|
+
if (intent?.kind === 'createIfAbsent' && files.has(target.path)) throw new Error('already exists')
|
|
30
|
+
files.set(target.path, content)
|
|
31
|
+
},
|
|
32
|
+
listDir: async target => [...new Set([...files.keys()].filter(path => path.startsWith(target.path + '\\'))
|
|
33
|
+
.map(path => path.slice(target.path.length + 1).split('\\')[0]))].map(name => ({ name })),
|
|
34
|
+
}
|
|
35
|
+
const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
|
|
36
|
+
if (name === 'sandboxPolicy') return { resolve: () => ({ mode: 'workspace-write', workspaceRoot: cwd, sessionId: session.id }) }
|
|
37
|
+
return undefined
|
|
38
|
+
} }
|
|
39
|
+
registerDevTask(ctx)
|
|
40
|
+
const exec = { agent: { session }, signal: new AbortController().signal }
|
|
41
|
+
return { cwd, files, fs, events, call: args => execute(args, exec), state: id => JSON.parse(files.get(join(cwd, `.dsh/task-${id}.json`))) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test('each task grade has a valid and increasing meta-skill sequence', () => {
|
|
45
|
+
assert.deepEqual(COMPLEXITY_OPTIONS.map(option => option.id), ['low', 'medium', 'high', 'ultra'])
|
|
46
|
+
const configs = COMPLEXITY_OPTIONS.map(option => adaptiveWorkflow(option.id))
|
|
47
|
+
for (const config of configs) assert.deepEqual(validateWorkflow(config), [])
|
|
48
|
+
assert.deepEqual(configs.map(config => config.stages.length), [4, 5, 6, 7])
|
|
49
|
+
assert.ok(configs[2].stages.includes('任务编排'))
|
|
50
|
+
assert.ok(configs[3].stages.includes('架构设计'))
|
|
51
|
+
assert.ok(configs.every(config => config.transitions.some(edge => edge.requires?.includes('verified'))))
|
|
52
|
+
assert.ok(configs.every(config => config.commit.checkpoints.includes('测试')))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('adaptive task freezes a project override and its own Rules, not the bundled same-name Skill', async () => {
|
|
56
|
+
const f = fixture({
|
|
57
|
+
'.dsh/skills/requirements-analysis/SKILL.md': '# Project requirement method',
|
|
58
|
+
'.dsh/skills/requirements-analysis/profile.json': JSON.stringify({ rules: [{ source: 'project', name: 'team-scope' }], evidence: 'artifact' }),
|
|
59
|
+
'.dsh/rules/team-scope.md': '# Team scope rule',
|
|
60
|
+
'.dsh/meta.json': JSON.stringify({ meta_bindings: { 'code-development': ['adaptive-project-project-map'] } }),
|
|
61
|
+
'.dsh/skills/adaptive-project-project-map/SKILL.md': '# Project map',
|
|
62
|
+
'.dsh/skills/adaptive-project-project-map/profile.json': JSON.stringify({ rules: [], evidence: 'none' }),
|
|
63
|
+
})
|
|
64
|
+
const preview = JSON.parse(await f.call({ operation: 'assess', complexity: 'high' }))
|
|
65
|
+
assert.equal(preview.meta_skills['需求分析'][0].effective_source, 'project')
|
|
66
|
+
assert.ok(preview.meta_skills['代码开发'].some(skill => skill.logical_name === 'adaptive-project-project-map'))
|
|
67
|
+
await f.call({ operation: 'create', task_id: 'A-1', title: 'cross-module change', branch: 'feature',
|
|
68
|
+
complexity: 'high', complexity_reason: 'Several implementation steps depend on earlier interfaces', files: ['app.js'] })
|
|
69
|
+
const state = f.state('A-1')
|
|
70
|
+
assert.equal(state.flow.flow, 'adaptive-high')
|
|
71
|
+
assert.equal(state.flow.config.stage_bindings['需求分析'].skills[0].skill.source, 'project')
|
|
72
|
+
assert.ok(state.flow.resources.some(entry => entry.ref.name === 'team-scope' && entry.ref.source === 'project'))
|
|
73
|
+
assert.match(await f.call({ operation: 'load_skill', task_id: 'A-1', skill_name: 'project:requirements-analysis' }), /Project requirement method/)
|
|
74
|
+
await assert.rejects(f.call({ operation: 'load_skill', task_id: 'A-1', skill_name: 'bundled:requirements-analysis' }), /exact source:name/)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('init_project proposes project-specific Skills and binds reviewed resources', async () => {
|
|
78
|
+
const f = fixture()
|
|
79
|
+
const inventory = JSON.parse(await f.call({ operation: 'init_project', phase: 'inspect' }))
|
|
80
|
+
assert.ok(inventory.suggestions.some(entry => entry.name === 'adaptive-project-project-map'))
|
|
81
|
+
assert.ok(inventory.manifests.some(entry => entry.facts.includes('vue=^2.7.0')))
|
|
82
|
+
const resources = [
|
|
83
|
+
{ kind: 'rule', name: 'component-boundary', content: 'Keep component boundary calls behind the existing service interface.' },
|
|
84
|
+
{ kind: 'skill', name: 'adaptive-project-project-map', description: 'Project structure and entry points',
|
|
85
|
+
content: 'The root package.json declares Vue 2.7; inspect src for the actual component layout.',
|
|
86
|
+
meta_skills: ['requirements-analysis', 'code-development'], rules: ['component-boundary'] },
|
|
87
|
+
]
|
|
88
|
+
const proposed = JSON.parse(await f.call({ operation: 'init_project', phase: 'propose', resources }))
|
|
89
|
+
assert.equal(f.files.has(join(f.cwd, '.dsh/meta.json')), false)
|
|
90
|
+
await f.call({ operation: 'init_project', phase: 'apply', resources, expected_hash: proposed.expected_hash,
|
|
91
|
+
existing_hash: proposed.existing_hash })
|
|
92
|
+
const meta = JSON.parse(f.files.get(join(f.cwd, '.dsh/meta.json')))
|
|
93
|
+
assert.deepEqual(meta.meta_bindings['code-development'], ['adaptive-project-project-map'])
|
|
94
|
+
assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/profile.json')))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('workbench adaptive settings persist meta bindings with a version check', async () => {
|
|
98
|
+
const f = fixture()
|
|
99
|
+
const receiver = { authorizedPath: async path => path, fs: () => f.fs,
|
|
100
|
+
listSkills: async () => ({ skills: [{ name: 'adaptive-project-project-map', source: 'project' }] }),
|
|
101
|
+
readAdaptive: Controller.prototype.readAdaptive }
|
|
102
|
+
const before = await Controller.prototype.readAdaptive.call(receiver, f.cwd)
|
|
103
|
+
assert.equal(before.source, 'default')
|
|
104
|
+
const config = { meta_bindings: { 'code-development': ['adaptive-project-project-map'] }, sonar: { enabled: false } }
|
|
105
|
+
const after = await Controller.prototype.writeAdaptive.call(receiver,
|
|
106
|
+
{ path: f.cwd, config, expected_hash: before.hash })
|
|
107
|
+
assert.equal(after.ok, true)
|
|
108
|
+
assert.deepEqual(after.config.meta_bindings['code-development'], ['adaptive-project-project-map'])
|
|
109
|
+
await assert.rejects(Controller.prototype.writeAdaptive.call(receiver,
|
|
110
|
+
{ path: f.cwd, config, expected_hash: before.hash }), /已被其他会话修改/)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('Sonar audit reads the exact CI analysis gate and filters new-code medium/high findings', async () => {
|
|
114
|
+
const calls = []
|
|
115
|
+
const fetcher = async (url, options) => {
|
|
116
|
+
calls.push({ url: String(url), authorization: options.headers.Authorization })
|
|
117
|
+
const body = String(url).includes('/ce/task')
|
|
118
|
+
? { task: { status: 'SUCCESS', analysisId: 'AN-1', componentKey: 'project' } }
|
|
119
|
+
: String(url).includes('/qualitygates/project_status')
|
|
120
|
+
? { projectStatus: { status: 'ERROR' } }
|
|
121
|
+
: { paging: { total: 2 }, issues: [
|
|
122
|
+
{ key: 'ISSUE-1', rule: 'java:S111', message: 'problem', severity: 'MAJOR', component: 'project:src/A.java', line: 3 },
|
|
123
|
+
{ key: 'ISSUE-2', rule: 'java:S222', message: 'minor', severity: 'MINOR', component: 'project:src/B.java' },
|
|
124
|
+
] }
|
|
125
|
+
return { ok: true, json: async () => body }
|
|
126
|
+
}
|
|
127
|
+
const policy = { enabled: true, host_url: 'https://sonar.example', project_key: 'project', mode: 'branch', token_env: 'SONAR_TOKEN' }
|
|
128
|
+
const audit = await inspectSonar(policy, 'secret-token', 'CE-TASK-123', 'feature', 'scope-hash', fetcher)
|
|
129
|
+
assert.equal(audit.analysis_id, 'AN-1')
|
|
130
|
+
assert.equal(audit.gate, 'ERROR')
|
|
131
|
+
assert.deepEqual(audit.blocking.map(issue => issue.key), ['ISSUE-1'])
|
|
132
|
+
assert.ok(calls[2].url.includes('inNewCodePeriod=true'))
|
|
133
|
+
assert.ok(calls.every(call => call.authorization === 'Bearer secret-token'))
|
|
134
|
+
assert.equal(JSON.stringify(audit).includes('secret-token'), false)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('review-only Sonar gate blocks pass until the task records a passing CI analysis', async () => {
|
|
138
|
+
const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true, host_url: 'https://sonar.example',
|
|
139
|
+
project_key: 'project', mode: 'branch', token_env: 'SONAR_TOKEN' } }) })
|
|
140
|
+
await f.call({ operation: 'create', task_id: 'S-1', title: 'change', branch: 'feature', complexity: 'low',
|
|
141
|
+
complexity_reason: 'One local change', files: ['app.js'], items: [{ id: 'one', title: 'change', status: 'done' }] })
|
|
142
|
+
const state = f.state('S-1')
|
|
143
|
+
state.stage = '代码审核'
|
|
144
|
+
state.verification = { passed: true, evidence: ['verified'] }
|
|
145
|
+
state.commits = [{ label: 'TASK', hash: 'abcdef1234567890' }]
|
|
146
|
+
f.files.set(join(f.cwd, '.dsh/task-S-1.json'), JSON.stringify(state))
|
|
147
|
+
await assert.rejects(f.call({ operation: 'review', task_id: 'S-1', outcome: 'pass' }), /SonarQube audit is enabled/)
|
|
148
|
+
const oldFetch = globalThis.fetch
|
|
149
|
+
const oldToken = process.env.SONAR_TOKEN
|
|
150
|
+
process.env.SONAR_TOKEN = 'secret-token'
|
|
151
|
+
globalThis.fetch = async url => ({ ok: true, json: async () => String(url).includes('/ce/task')
|
|
152
|
+
? { task: { status: 'SUCCESS', analysisId: 'AN-2', componentKey: 'project' } }
|
|
153
|
+
: String(url).includes('/qualitygates/project_status') ? { projectStatus: { status: 'OK' } }
|
|
154
|
+
: { paging: { total: 0 }, issues: [] } })
|
|
155
|
+
try {
|
|
156
|
+
await f.call({ operation: 'sonar_check', task_id: 'S-1', ce_task_id: 'CE-123456' })
|
|
157
|
+
await f.call({ operation: 'review', task_id: 'S-1', outcome: 'pass' })
|
|
158
|
+
assert.equal(f.state('S-1').review.outcome, 'pass')
|
|
159
|
+
} finally {
|
|
160
|
+
globalThis.fetch = oldFetch
|
|
161
|
+
if (oldToken === undefined) delete process.env.SONAR_TOKEN
|
|
162
|
+
else process.env.SONAR_TOKEN = oldToken
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('a failed Sonar finding becomes a reviewed project Rule attached to the coding Skill', async () => {
|
|
167
|
+
const f = fixture()
|
|
168
|
+
await f.call({ operation: 'create', task_id: 'L-1', title: 'change', branch: 'feature', complexity: 'low',
|
|
169
|
+
complexity_reason: 'One local change', files: ['app.js'] })
|
|
170
|
+
const state = f.state('L-1')
|
|
171
|
+
const finding = { key: 'ISSUE-9', rule: 'java:S999', message: 'Handle null', severity: 'MAJOR', file: 'A.java' }
|
|
172
|
+
state.stage = '代码审核'
|
|
173
|
+
state.sonar_audit = { ce_task_id: 'CE-123456', analysis_id: 'AN-9', gate: 'ERROR', checked_at: 'now',
|
|
174
|
+
scope_hash: 'old', findings: [finding], blocking: [finding], target: 'feature' }
|
|
175
|
+
f.files.set(join(f.cwd, '.dsh/task-L-1.json'), JSON.stringify(state))
|
|
176
|
+
await f.call({ operation: 'revise', task_id: 'L-1', target_stage: '代码开发',
|
|
177
|
+
revision_kind: 'defect', revision_reason: 'Fix the SonarQube issue' })
|
|
178
|
+
assert.equal(f.state('L-1').sonar_history[0].blocking[0].key, 'ISSUE-9')
|
|
179
|
+
const args = { operation: 'learn_rule', task_id: 'L-1', issue_key: 'ISSUE-9', skill_name: 'bundled:code-development',
|
|
180
|
+
rule_name: 'handle-null-boundary', learning_reason: 'This boundary appears in several service methods',
|
|
181
|
+
content: 'At a service boundary, check for null before dereferencing. Show the failing call and a guarded call in the review example.' }
|
|
182
|
+
const proposal = JSON.parse(await f.call({ ...args, phase: 'propose' }))
|
|
183
|
+
assert.equal(f.files.has(join(f.cwd, '.dsh/rules/handle-null-boundary.md')), false)
|
|
184
|
+
await f.call({ ...args, phase: 'apply', expected_hash: proposal.expected_hash })
|
|
185
|
+
assert.match(f.files.get(join(f.cwd, '.dsh/rules/handle-null-boundary.md')), /java:S999/)
|
|
186
|
+
assert.ok(f.files.has(join(f.cwd, '.dsh/skills/code-development/SKILL.md')))
|
|
187
|
+
assert.deepEqual(JSON.parse(f.files.get(join(f.cwd, '.dsh/skills/code-development/profile.json'))).rules,
|
|
188
|
+
[{ source: 'project', name: 'handle-null-boundary' }])
|
|
189
|
+
})
|
package/.assessment-batch1.mjs
CHANGED
|
@@ -320,18 +320,18 @@ test('batch3: the same skill carries identical rules wherever it is bound', () =
|
|
|
320
320
|
test('batch3: a rule shared by two skills is one resource, referenced twice', () => {
|
|
321
321
|
// Reuse must not duplicate the rule body: two skills referencing one rule point
|
|
322
322
|
// at the same source-qualified reference.
|
|
323
|
-
const sharedRule = { source: 'project', name: 'shared-rule' }
|
|
324
|
-
const config = resolveFlow('standard', { flow: 'standard', stage_bindings: {
|
|
325
|
-
'开发': { skills: [{ skill: { source: 'project', name: 'implement' }, rules: [sharedRule] }] },
|
|
326
|
-
'代码审核': { skills: [{ skill: { source: 'project', name: 'review' }, rules: [sharedRule] }] },
|
|
327
|
-
} }).config
|
|
328
|
-
const reviewRules = config.stage_bindings['代码审核'].skills[0].rules.map(formatResourceRef)
|
|
329
|
-
const implementRules = config.stage_bindings['开发'].skills[0].rules.map(formatResourceRef)
|
|
323
|
+
const sharedRule = { source: 'project', name: 'shared-rule' }
|
|
324
|
+
const config = resolveFlow('standard', { flow: 'standard', stage_bindings: {
|
|
325
|
+
'开发': { skills: [{ skill: { source: 'project', name: 'implement' }, rules: [sharedRule] }] },
|
|
326
|
+
'代码审核': { skills: [{ skill: { source: 'project', name: 'review' }, rules: [sharedRule] }] },
|
|
327
|
+
} }).config
|
|
328
|
+
const reviewRules = config.stage_bindings['代码审核'].skills[0].rules.map(formatResourceRef)
|
|
329
|
+
const implementRules = config.stage_bindings['开发'].skills[0].rules.map(formatResourceRef)
|
|
330
330
|
const shared = reviewRules.filter(rule => implementRules.includes(rule))
|
|
331
331
|
assert.ok(shared.length > 0,
|
|
332
332
|
'coding-conventions and security-redlines apply to both implementing and reviewing code, so at least one rule must be shared by reference')
|
|
333
|
-
assert.ok(shared.includes('project:shared-rule'),
|
|
334
|
-
`the shared project rule is one reference; saw ${JSON.stringify(shared)}`)
|
|
333
|
+
assert.ok(shared.includes('project:shared-rule'),
|
|
334
|
+
`the shared project rule is one reference; saw ${JSON.stringify(shared)}`)
|
|
335
335
|
})
|
|
336
336
|
|
|
337
337
|
test('batch3: legacy stage-level rules stay in force and are named as unassigned', async () => {
|
|
@@ -339,17 +339,17 @@ test('batch3: legacy stage-level rules stay in force and are named as unassigned
|
|
|
339
339
|
// invent an answer the config never had; dropping them would lose a constraint.
|
|
340
340
|
const f = fixture('standard', '开发', {}, {})
|
|
341
341
|
const state = f.state()
|
|
342
|
-
state.flow.config.stage_bindings = { 开发: { skills: [], legacy_rules: ['shared-rule'] } }
|
|
343
|
-
f.records.set(join(f.cwd, '.dsh/rules/shared-rule.md'), '# shared legacy rule')
|
|
342
|
+
state.flow.config.stage_bindings = { 开发: { skills: [], legacy_rules: ['shared-rule'] } }
|
|
343
|
+
f.records.set(join(f.cwd, '.dsh/rules/shared-rule.md'), '# shared legacy rule')
|
|
344
344
|
f.records.set(join(f.cwd, '.dsh/task-LIVE-1.json'), JSON.stringify(state))
|
|
345
345
|
const status = JSON.parse(await f.call({ operation: 'status' }))
|
|
346
346
|
assert.ok(
|
|
347
|
-
(status.unassigned_legacy_rules ?? []).includes('shared-rule'),
|
|
347
|
+
(status.unassigned_legacy_rules ?? []).includes('shared-rule'),
|
|
348
348
|
`the legacy rule must be reported as unassigned; saw ${JSON.stringify(status.unassigned_legacy_rules)}`,
|
|
349
349
|
)
|
|
350
350
|
// Still enforced: it resolves and appears among the rules in force.
|
|
351
351
|
assert.ok(
|
|
352
|
-
(status.rules ?? []).some(rule => rule.name === 'shared-rule'),
|
|
352
|
+
(status.rules ?? []).some(rule => rule.name === 'shared-rule'),
|
|
353
353
|
`a legacy rule must keep applying; saw ${JSON.stringify(status.rules)}`,
|
|
354
354
|
)
|
|
355
355
|
})
|
|
@@ -423,4 +423,4 @@ test('P2: an unknown review_depth is a configuration error, not a silent downgra
|
|
|
423
423
|
const problems = validateWorkflow(config)
|
|
424
424
|
assert.ok(problems.some(p => /review_depth/u.test(p)),
|
|
425
425
|
`a typo must be reported; saw ${JSON.stringify(problems)}`)
|
|
426
|
-
})
|
|
426
|
+
})
|
package/.codex-project-test.mjs
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import test from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
4
|
-
import { tmpdir } from 'node:os'
|
|
5
|
-
import { dirname, join, resolve, sep } from 'node:path'
|
|
6
|
-
import Controller from './lib/controller.js'
|
|
7
|
-
import { registerDevTask } from './lib/dev-task.js'
|
|
8
|
-
import { loadedSkills, skillBlockers } from './lib/skill-audit.js'
|
|
9
|
-
import { resolveFlow } from './lib/workflows.js'
|
|
10
|
-
import { validateWorkflow } from './lib/engine.js'
|
|
11
|
-
|
|
12
|
-
function fixture(t) {
|
|
13
|
-
const root = mkdtempSync(join(tmpdir(), 'dsh-codex-project-'))
|
|
14
|
-
assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep))
|
|
15
|
-
t.after(() => rmSync(root, { recursive: true, force: true }))
|
|
16
|
-
return root
|
|
17
|
-
}
|
|
18
|
-
function put(path, body) {
|
|
19
|
-
mkdirSync(dirname(path), { recursive: true })
|
|
20
|
-
writeFileSync(path, body)
|
|
21
|
-
}
|
|
22
|
-
function diskFs() {
|
|
23
|
-
return {
|
|
24
|
-
resolve: async (path, options) => resolve(options?.cwd ?? '', path),
|
|
25
|
-
readText: async path => { try { return readFileSync(path, 'utf8') } catch { return undefined } },
|
|
26
|
-
lstat: async (path, options) => { try { return { version: String(statSync(resolve(options?.cwd ?? '', path)).mtimeMs) } } catch { return undefined } },
|
|
27
|
-
writeText: async (path, body) => put(path, body),
|
|
28
|
-
listDir: async path => readdirSync(path, { withFileTypes: true }).map(entry => ({ name: entry.name })),
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
test('Codex project Skills are discovered separately; new DSH Skills stay in .dsh', async t => {
|
|
33
|
-
const root = fixture(t)
|
|
34
|
-
const body = '---\nname: same\ndescription: Codex method\n---\nCodex body v1\n'
|
|
35
|
-
put(join(root, '.agents/skills/same/SKILL.md'), body)
|
|
36
|
-
put(join(root, '.dsh/skills/same/SKILL.md'), '---\nname: same\ndescription: DSH method\n---\nDSH body\n')
|
|
37
|
-
const receiver = { authorizedPath: async path => path }
|
|
38
|
-
const catalog = await Controller.prototype.listSkills.call(receiver, root)
|
|
39
|
-
assert.deepEqual(catalog.skills.filter(skill => skill.name === 'same').map(skill => skill.ref.source).sort(), ['codex-project', 'project'])
|
|
40
|
-
const read = await Controller.prototype.readSkill.call(receiver, { level: 'codex-project', name: 'same', path: root })
|
|
41
|
-
assert.equal(read.content, 'Codex body v1')
|
|
42
|
-
const created = await Controller.prototype.writeSkill.call(receiver, { level: 'project', name: 'new-one', description: 'New DSH method', whenToUse: '', content: 'body', path: root })
|
|
43
|
-
assert.equal(created.ok, true)
|
|
44
|
-
assert.equal(readFileSync(join(root, '.dsh/skills/new-one/SKILL.md'), 'utf8').includes('body'), true)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('a bound Codex Skill loads its attached DSH Rule and reads edits next time', async t => {
|
|
48
|
-
const root = fixture(t)
|
|
49
|
-
const skillFile = join(root, '.agents/skills/sample/SKILL.md')
|
|
50
|
-
const ruleFile = join(root, '.dsh/rules/shared.md')
|
|
51
|
-
put(skillFile, '---\nname: sample\ndescription: Sample\n---\nSkill v1\n')
|
|
52
|
-
put(ruleFile, 'Rule v1')
|
|
53
|
-
const ref = { source: 'codex-project', name: 'sample' }
|
|
54
|
-
const rule = { source: 'project', name: 'shared' }
|
|
55
|
-
const project = { flow: 'minimal', stage_bindings: { '开发': { skill_refs: [ref] } }, skill_profiles: { 'codex-project:sample': { rules: [rule], evidence: 'none' } } }
|
|
56
|
-
put(join(root, '.dsh/eng.json'), JSON.stringify(project))
|
|
57
|
-
assert.deepEqual(validateWorkflow(resolveFlow('minimal', project).config), [])
|
|
58
|
-
const fs = diskFs(), tools = []
|
|
59
|
-
registerDevTask({ fs, tools: { register(tool) { tools.push(tool) } }, get() { return undefined } })
|
|
60
|
-
const execute = tools[0].execute
|
|
61
|
-
const events = []
|
|
62
|
-
const session = { id: 'test-session', header: { cwd: root }, snapshotEvents: () => events }
|
|
63
|
-
const run = (args, callId = 'call') => execute(args, { agent: { session }, callId, signal: undefined })
|
|
64
|
-
await run({ operation: 'create', task_id: 'T1', title: 'Test', branch: 'main' })
|
|
65
|
-
const first = await run({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }, 'load-1')
|
|
66
|
-
assert.match(first, /Skill v1/)
|
|
67
|
-
assert.match(first, /Rule v1/)
|
|
68
|
-
events.push({ type: 'tool/call', data: { name: 'dev_task', callId: 'load-1', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }) } })
|
|
69
|
-
events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: 'load-1', isError: false }] } } })
|
|
70
|
-
assert.equal(loadedSkills(session).get('T1#codex-project:sample'), 'load-1')
|
|
71
|
-
const state = JSON.parse(readFileSync(join(root, '.dsh/task-T1.json'), 'utf8'))
|
|
72
|
-
assert.deepEqual(skillBlockers(state, state.flow.config, session), [])
|
|
73
|
-
assert.match(skillBlockers({ ...state, id: 'T2' }, state.flow.config, session).join('; '), /load_skill/,
|
|
74
|
-
'loading a Skill for one task must not satisfy another task with a different Rule profile')
|
|
75
|
-
put(skillFile, '---\nname: sample\ndescription: Sample\n---\nSkill v2\n')
|
|
76
|
-
put(ruleFile, 'Rule v2')
|
|
77
|
-
const second = await run({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }, 'load-2')
|
|
78
|
-
assert.match(second, /Skill v2/)
|
|
79
|
-
assert.match(second, /Rule v2/)
|
|
80
|
-
})
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
6
|
+
import Controller from './lib/controller.js'
|
|
7
|
+
import { registerDevTask } from './lib/dev-task.js'
|
|
8
|
+
import { loadedSkills, skillBlockers } from './lib/skill-audit.js'
|
|
9
|
+
import { resolveFlow } from './lib/workflows.js'
|
|
10
|
+
import { validateWorkflow } from './lib/engine.js'
|
|
11
|
+
|
|
12
|
+
function fixture(t) {
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-codex-project-'))
|
|
14
|
+
assert.ok(resolve(root).startsWith(resolve(tmpdir()) + sep))
|
|
15
|
+
t.after(() => rmSync(root, { recursive: true, force: true }))
|
|
16
|
+
return root
|
|
17
|
+
}
|
|
18
|
+
function put(path, body) {
|
|
19
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
20
|
+
writeFileSync(path, body)
|
|
21
|
+
}
|
|
22
|
+
function diskFs() {
|
|
23
|
+
return {
|
|
24
|
+
resolve: async (path, options) => resolve(options?.cwd ?? '', path),
|
|
25
|
+
readText: async path => { try { return readFileSync(path, 'utf8') } catch { return undefined } },
|
|
26
|
+
lstat: async (path, options) => { try { return { version: String(statSync(resolve(options?.cwd ?? '', path)).mtimeMs) } } catch { return undefined } },
|
|
27
|
+
writeText: async (path, body) => put(path, body),
|
|
28
|
+
listDir: async path => readdirSync(path, { withFileTypes: true }).map(entry => ({ name: entry.name })),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('Codex project Skills are discovered separately; new DSH Skills stay in .dsh', async t => {
|
|
33
|
+
const root = fixture(t)
|
|
34
|
+
const body = '---\nname: same\ndescription: Codex method\n---\nCodex body v1\n'
|
|
35
|
+
put(join(root, '.agents/skills/same/SKILL.md'), body)
|
|
36
|
+
put(join(root, '.dsh/skills/same/SKILL.md'), '---\nname: same\ndescription: DSH method\n---\nDSH body\n')
|
|
37
|
+
const receiver = { authorizedPath: async path => path }
|
|
38
|
+
const catalog = await Controller.prototype.listSkills.call(receiver, root)
|
|
39
|
+
assert.deepEqual(catalog.skills.filter(skill => skill.name === 'same').map(skill => skill.ref.source).sort(), ['codex-project', 'project'])
|
|
40
|
+
const read = await Controller.prototype.readSkill.call(receiver, { level: 'codex-project', name: 'same', path: root })
|
|
41
|
+
assert.equal(read.content, 'Codex body v1')
|
|
42
|
+
const created = await Controller.prototype.writeSkill.call(receiver, { level: 'project', name: 'new-one', description: 'New DSH method', whenToUse: '', content: 'body', path: root })
|
|
43
|
+
assert.equal(created.ok, true)
|
|
44
|
+
assert.equal(readFileSync(join(root, '.dsh/skills/new-one/SKILL.md'), 'utf8').includes('body'), true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('a bound Codex Skill loads its attached DSH Rule and reads edits next time', async t => {
|
|
48
|
+
const root = fixture(t)
|
|
49
|
+
const skillFile = join(root, '.agents/skills/sample/SKILL.md')
|
|
50
|
+
const ruleFile = join(root, '.dsh/rules/shared.md')
|
|
51
|
+
put(skillFile, '---\nname: sample\ndescription: Sample\n---\nSkill v1\n')
|
|
52
|
+
put(ruleFile, 'Rule v1')
|
|
53
|
+
const ref = { source: 'codex-project', name: 'sample' }
|
|
54
|
+
const rule = { source: 'project', name: 'shared' }
|
|
55
|
+
const project = { flow: 'minimal', stage_bindings: { '开发': { skill_refs: [ref] } }, skill_profiles: { 'codex-project:sample': { rules: [rule], evidence: 'none' } } }
|
|
56
|
+
put(join(root, '.dsh/eng.json'), JSON.stringify(project))
|
|
57
|
+
assert.deepEqual(validateWorkflow(resolveFlow('minimal', project).config), [])
|
|
58
|
+
const fs = diskFs(), tools = []
|
|
59
|
+
registerDevTask({ fs, tools: { register(tool) { tools.push(tool) } }, get() { return undefined } })
|
|
60
|
+
const execute = tools[0].execute
|
|
61
|
+
const events = []
|
|
62
|
+
const session = { id: 'test-session', header: { cwd: root }, snapshotEvents: () => events }
|
|
63
|
+
const run = (args, callId = 'call') => execute(args, { agent: { session }, callId, signal: undefined })
|
|
64
|
+
await run({ operation: 'create', task_id: 'T1', title: 'Test', branch: 'main' })
|
|
65
|
+
const first = await run({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }, 'load-1')
|
|
66
|
+
assert.match(first, /Skill v1/)
|
|
67
|
+
assert.match(first, /Rule v1/)
|
|
68
|
+
events.push({ type: 'tool/call', data: { name: 'dev_task', callId: 'load-1', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }) } })
|
|
69
|
+
events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: 'load-1', isError: false }] } } })
|
|
70
|
+
assert.equal(loadedSkills(session).get('T1#codex-project:sample'), 'load-1')
|
|
71
|
+
const state = JSON.parse(readFileSync(join(root, '.dsh/task-T1.json'), 'utf8'))
|
|
72
|
+
assert.deepEqual(skillBlockers(state, state.flow.config, session), [])
|
|
73
|
+
assert.match(skillBlockers({ ...state, id: 'T2' }, state.flow.config, session).join('; '), /load_skill/,
|
|
74
|
+
'loading a Skill for one task must not satisfy another task with a different Rule profile')
|
|
75
|
+
put(skillFile, '---\nname: sample\ndescription: Sample\n---\nSkill v2\n')
|
|
76
|
+
put(ruleFile, 'Rule v2')
|
|
77
|
+
const second = await run({ operation: 'load_skill', task_id: 'T1', skill_name: 'codex-project:sample' }, 'load-2')
|
|
78
|
+
assert.match(second, /Skill v2/)
|
|
79
|
+
assert.match(second, /Rule v2/)
|
|
80
|
+
})
|
package/.enforce-test.mjs
CHANGED
|
@@ -15,13 +15,13 @@ import { completionBlockers, terminalRequirements, resourceBlockers } from './li
|
|
|
15
15
|
async function project() {
|
|
16
16
|
const cwd = resolve('enforce-project')
|
|
17
17
|
const adopted = adoptRecommendation('standard')
|
|
18
|
-
const skills = [{ skill: { source: 'project', name: 'my-implementation' },
|
|
19
|
-
rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
|
|
18
|
+
const skills = [{ skill: { source: 'project', name: 'my-implementation' },
|
|
19
|
+
rules: [{ source: 'project', name: 'mine' }], evidence: 'none' }]
|
|
20
20
|
const stage_bindings = { ...adopted.stage_bindings, '开发': { skills } }
|
|
21
21
|
const records = new Map([
|
|
22
22
|
[join(cwd, '.dsh/eng.json'), JSON.stringify({ ...adopted, stage_bindings })],
|
|
23
|
-
[join(cwd, '.dsh/rules/mine.md'), 'RULE'],
|
|
24
|
-
[join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '---\nname: my-implementation\n---\nImplement.'],
|
|
23
|
+
[join(cwd, '.dsh/rules/mine.md'), 'RULE'],
|
|
24
|
+
[join(cwd, '.dsh/skills/my-implementation/SKILL.md'), '---\nname: my-implementation\n---\nImplement.'],
|
|
25
25
|
[join(cwd, 'a.js'), 'source'],
|
|
26
26
|
])
|
|
27
27
|
const events = []
|
|
@@ -85,13 +85,13 @@ test('enforce: a task WITHOUT a snapshot cannot advance while a bound rule is un
|
|
|
85
85
|
)
|
|
86
86
|
})
|
|
87
87
|
|
|
88
|
-
test('enforce: a task WITH a snapshot blocks when its live source rule is deleted', async () => {
|
|
89
|
-
const f = await project()
|
|
90
|
-
await satistfyDevelopment(f)
|
|
91
|
-
f.records.delete(f.rule())
|
|
92
|
-
await assert.rejects(() => f.call({ operation: 'advance', target_stage: '交付' }), /resolves nowhere/u)
|
|
93
|
-
assert.equal(f.current().stage, '开发')
|
|
94
|
-
})
|
|
88
|
+
test('enforce: a task WITH a snapshot blocks when its live source rule is deleted', async () => {
|
|
89
|
+
const f = await project()
|
|
90
|
+
await satistfyDevelopment(f)
|
|
91
|
+
f.records.delete(f.rule())
|
|
92
|
+
await assert.rejects(() => f.call({ operation: 'advance', target_stage: '交付' }), /resolves nowhere/u)
|
|
93
|
+
assert.equal(f.current().stage, '开发')
|
|
94
|
+
})
|
|
95
95
|
|
|
96
96
|
test('enforce: completion requires the review and verification the flow declared', async () => {
|
|
97
97
|
// Arriving at the last stage used to be enough. The agile flow declares a review
|
|
@@ -131,4 +131,4 @@ test('enforce: the resource blocker names the stage, the reference and the fix',
|
|
|
131
131
|
assert.match(blockers[0], /Restore the file|remove the binding/u,
|
|
132
132
|
'a blocker must say what to do, not only what is wrong')
|
|
133
133
|
assert.deepEqual(resourceBlockers([], '开发'), [], 'a resolvable stage has no blocker')
|
|
134
|
-
})
|
|
134
|
+
})
|
package/.evidence-test.mjs
CHANGED
|
@@ -24,7 +24,7 @@ function sessionWith(names) {
|
|
|
24
24
|
/** A standard-flow config with one extra skill on 开发 carrying the given evidence. */
|
|
25
25
|
function withEvidence(evidence) {
|
|
26
26
|
const adopted = adoptRecommendation('standard')
|
|
27
|
-
const skills = [{ skill: { source: 'project', name: 'doc-skill' }, rules: [], ...(evidence !== undefined ? { evidence } : {}) }]
|
|
27
|
+
const skills = [{ skill: { source: 'project', name: 'doc-skill' }, rules: [], ...(evidence !== undefined ? { evidence } : {}) }]
|
|
28
28
|
return resolveFlow('standard', { ...adopted, stage_bindings: { ...adopted.stage_bindings, '开发': { skills } } }).config
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -42,8 +42,8 @@ function state(extra = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
test('evidence: needsSkillReceipt honours a declared non-command kind', () => {
|
|
45
|
-
// No name-based exemption remains; a declared kind overrides the default.
|
|
46
|
-
assert.equal(needsSkillReceipt('code-implement'), true, 'skill names do not exempt command evidence')
|
|
45
|
+
// No name-based exemption remains; a declared kind overrides the default.
|
|
46
|
+
assert.equal(needsSkillReceipt('code-implement'), true, 'skill names do not exempt command evidence')
|
|
47
47
|
assert.equal(needsSkillReceipt('doc-skill'), true, 'an undeclared extra skill defaults to a command receipt')
|
|
48
48
|
assert.equal(needsSkillReceipt('doc-skill', 'command'), true)
|
|
49
49
|
assert.equal(needsSkillReceipt('doc-skill', 'artifact'), false)
|
|
@@ -56,8 +56,8 @@ test('evidence: a document-producing skill is satisfied by a recorded artifact',
|
|
|
56
56
|
// The blocker is the point: without an artifact it blocks, and with one it does
|
|
57
57
|
// not — so the declared kind is genuinely enforced rather than merely accepted.
|
|
58
58
|
//
|
|
59
|
-
// Mounted on 设计, which the adopted setup declares an artifact for. Missing
|
|
60
|
-
// declarations and incomplete fields must both remain blockers.
|
|
59
|
+
// Mounted on 设计, which the adopted setup declares an artifact for. Missing
|
|
60
|
+
// declarations and incomplete fields must both remain blockers.
|
|
61
61
|
const adopted = adoptRecommendation('standard')
|
|
62
62
|
const config = resolveFlow('standard', {
|
|
63
63
|
...adopted,
|
|
@@ -78,7 +78,7 @@ test('evidence: a document-producing skill is satisfied by a recorded artifact',
|
|
|
78
78
|
// Record what the stage declares and the blocker clears.
|
|
79
79
|
const declared = config.artifacts.find(artifact => artifact.stage === '设计')
|
|
80
80
|
assert.ok(declared !== undefined, 'the standard flow declares an artifact at 设计')
|
|
81
|
-
const cleared = skillBlockers(atDesign({ artifacts: { [declared.id]: Object.fromEntries(declared.fields.map(field => [field, 'checked'])) } }), config, session)
|
|
81
|
+
const cleared = skillBlockers(atDesign({ artifacts: { [declared.id]: Object.fromEntries(declared.fields.map(field => [field, 'checked'])) } }), config, session)
|
|
82
82
|
assert.deepEqual(cleared.filter(b => b.includes('doc-skill')), [],
|
|
83
83
|
'a recorded artifact satisfies artifact evidence')
|
|
84
84
|
})
|
|
@@ -98,7 +98,7 @@ test('evidence: manual evidence needs an explicit statement, none needs nothing'
|
|
|
98
98
|
const session = sessionWith(['code-implement', 'doc-skill'])
|
|
99
99
|
assert.ok(skillBlockers(state(), manual, session).some(b => b.includes('manual evidence')),
|
|
100
100
|
'manual evidence is absent until something is recorded')
|
|
101
|
-
const recorded = state({ skill_results: { '开发': { 'doc-skill': { evidence: ['reviewed by the lead'], approved: true } } } })
|
|
101
|
+
const recorded = state({ skill_results: { '开发': { 'doc-skill': { evidence: ['reviewed by the lead'], approved: true } } } })
|
|
102
102
|
assert.deepEqual(skillBlockers(recorded, manual, session).filter(b => b.includes('doc-skill')), [],
|
|
103
103
|
'a recorded statement satisfies manual evidence')
|
|
104
104
|
|
|
@@ -115,4 +115,4 @@ test('evidence: an undeclared extra skill still owes a command receipt', () => {
|
|
|
115
115
|
const blockers = skillBlockers(state(), config, session)
|
|
116
116
|
assert.ok(blockers.some(b => b.includes('validation command')),
|
|
117
117
|
`an undeclared skill must still owe a command receipt; got ${JSON.stringify(blockers)}`)
|
|
118
|
-
})
|
|
118
|
+
})
|
package/.filter-test.mjs
CHANGED
|
@@ -83,9 +83,9 @@ test('filter: only-selected combined with a query still keeps what is in force',
|
|
|
83
83
|
assert.ok(!shown.some(e => e.name === 'code-implement'), 'an unbound non-matching skill is dropped')
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
test('filter: a fresh project has no prebound business skill', () => {
|
|
87
|
-
const adopted = adoptRecommendation('standard')
|
|
88
|
-
const resolved = resolveFlow('standard', adopted).config
|
|
89
|
-
const binding = bindingsForStage('开发', resolved)
|
|
90
|
-
assert.equal(binding, undefined)
|
|
91
|
-
})
|
|
86
|
+
test('filter: a fresh project has no prebound business skill', () => {
|
|
87
|
+
const adopted = adoptRecommendation('standard')
|
|
88
|
+
const resolved = resolveFlow('standard', adopted).config
|
|
89
|
+
const binding = bindingsForStage('开发', resolved)
|
|
90
|
+
assert.equal(binding, undefined)
|
|
91
|
+
})
|