@godv61/dsh-task-engine 0.29.8 → 0.30.1

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.
Files changed (67) hide show
  1. package/.adaptive-test.mjs +257 -178
  2. package/.evidence-test.mjs +16 -16
  3. package/.resource-test.mjs +19 -19
  4. package/.roundtrip-test.mjs +5 -5
  5. package/.sonar-credential-test.mjs +38 -38
  6. package/.sonarlint-local-test.mjs +67 -61
  7. package/.workflow-test.mjs +51 -21
  8. package/README.md +216 -114
  9. package/docs/development.md +45 -53
  10. package/docs/manual.html +176 -127
  11. package/hooks/commit-msg +18 -0
  12. package/lib/client.js +261 -37
  13. package/lib/client.js.map +2 -2
  14. package/lib/controller.d.ts +42 -0
  15. package/lib/controller.js +234 -1
  16. package/lib/controller.js.map +1 -1
  17. package/lib/dev-task.js +93 -50
  18. package/lib/dev-task.js.map +1 -1
  19. package/lib/engine.d.ts +4 -0
  20. package/lib/engine.js +6 -0
  21. package/lib/engine.js.map +1 -1
  22. package/lib/project-init.d.ts +11 -0
  23. package/lib/project-init.js +32 -1
  24. package/lib/project-init.js.map +1 -1
  25. package/lib/sonar-report.d.ts +1 -1
  26. package/lib/sonar-report.js +13 -0
  27. package/lib/sonar-report.js.map +1 -1
  28. package/lib/sonar.d.ts +12 -0
  29. package/lib/sonar.js +8 -0
  30. package/lib/sonar.js.map +1 -1
  31. package/lib/sonarlint-local.d.ts +2 -0
  32. package/lib/sonarlint-local.js +11 -1
  33. package/lib/sonarlint-local.js.map +1 -1
  34. package/lib/verification-tests.d.ts +10 -0
  35. package/lib/verification-tests.js +15 -0
  36. package/lib/verification-tests.js.map +1 -0
  37. package/package.json +10 -10
  38. package/scripts/verify-package.mjs +3 -1
  39. package/skills/code-review/SKILL.md +3 -1
  40. package/skills/eng-delivery/SKILL.md +5 -4
  41. package/skills/task-orchestration/SKILL.md +1 -1
  42. package/skills/test-validation/SKILL.md +3 -1
  43. package/docs/BRIEF-FOR-REVIEW.md +0 -163
  44. package/docs/CHANGELOG.md +0 -421
  45. package/docs/README.md +0 -42
  46. package/docs/adaptive-workflows.md +0 -88
  47. package/docs/assets/workflow-banner.svg +0 -29
  48. package/docs/configuration.md +0 -80
  49. package/docs/faq.md +0 -65
  50. package/docs/getting-started.md +0 -55
  51. package/docs/listing/godv61__dsh-task-engine.yml +0 -6
  52. package/docs/listing/submission.md +0 -84
  53. package/docs/manual-legacy.html +0 -380
  54. package/docs/releases/0.23.0.md +0 -32
  55. package/docs/releases/0.23.1.md +0 -58
  56. package/docs/releases/0.23.2.md +0 -21
  57. package/docs/resource-install.md +0 -64
  58. package/docs/roadmap.md +0 -33
  59. package/docs/testing/0.23.0//346/265/213/350/257/225/346/211/247/350/241/214/350/256/260/345/275/225.md +0 -189
  60. package/docs/testing/0.23.0//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -42
  61. package/docs/testing/0.23.1//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -34
  62. package/docs/testing/0.23.1//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -41
  63. 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 +0 -56
  64. 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 +0 -38
  65. package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +0 -65
  66. package/docs/testing/0.23.2//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/346/230/216/347/273/206.md +0 -43
  67. package/docs/workflow-regression.md +0 -36
@@ -1,19 +1,19 @@
1
1
  import test from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
3
  import { join, resolve } from 'node:path'
4
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
5
5
  import { tmpdir } from 'node:os'
6
6
  import { adaptiveWorkflow, COMPLEXITY_OPTIONS } from './lib/adaptive.js'
7
7
  import { validateWorkflow } from './lib/engine.js'
8
- import { plannedItemIds, registerDevTask } from './lib/dev-task.js'
9
- import { assertLocalSonarReady, inspectSonar, validLocalScanCommand } from './lib/sonar.js'
8
+ import { plannedItemIds, registerDevTask } from './lib/dev-task.js'
9
+ import { assertLocalSonarReady, inspectSonar, validLocalScanCommand } from './lib/sonar.js'
10
10
  import Controller from './lib/controller.js'
11
11
 
12
12
  const home = mkdtempSync(join(tmpdir(), 'dsh-adaptive-'))
13
13
  process.env.DSH_HOME = home
14
14
  process.on('exit', () => rmSync(home, { recursive: true, force: true }))
15
15
 
16
- function fixture(extra = {}, services = {}) {
16
+ function fixture(extra = {}, services = {}) {
17
17
  const cwd = resolve('adaptive-project')
18
18
  const files = new Map([[join(cwd, 'package.json'), '{"name":"adaptive-project","dependencies":{"vue":"^2.7.0"}}'],
19
19
  [join(cwd, '.git/HEAD'), 'ref: refs/heads/feature'] , ...Object.entries(extra).map(([p, content]) => [join(cwd, p), content])])
@@ -32,9 +32,9 @@ function fixture(extra = {}, services = {}) {
32
32
  listDir: async target => [...new Set([...files.keys()].filter(path => path.startsWith(target.path + '\\'))
33
33
  .map(path => path.slice(target.path.length + 1).split('\\')[0]))].map(name => ({ name })),
34
34
  }
35
- const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
36
- if (name in services) return services[name]
37
- if (name === 'sandboxPolicy') return { resolve: () => ({ mode: 'workspace-write', workspaceRoot: cwd, sessionId: session.id }) }
35
+ const ctx = { fs, tools: { register(tool) { execute = tool.execute; return () => {} } }, get(name) {
36
+ if (name in services) return services[name]
37
+ if (name === 'sandboxPolicy') return { resolve: () => ({ mode: 'workspace-write', workspaceRoot: cwd, sessionId: session.id }) }
38
38
  return undefined
39
39
  } }
40
40
  registerDevTask(ctx)
@@ -42,7 +42,7 @@ function fixture(extra = {}, services = {}) {
42
42
  return { cwd, files, fs, events, call: args => execute(args, exec), state: id => JSON.parse(files.get(join(cwd, `.dsh/task-${id}.json`))) }
43
43
  }
44
44
 
45
- test('each task grade has a valid and increasing meta-skill sequence', () => {
45
+ test('each task grade has a valid and increasing meta-skill sequence', () => {
46
46
  assert.deepEqual(COMPLEXITY_OPTIONS.map(option => option.id), ['low', 'medium', 'high', 'ultra'])
47
47
  const configs = COMPLEXITY_OPTIONS.map(option => adaptiveWorkflow(option.id))
48
48
  for (const config of configs) assert.deepEqual(validateWorkflow(config), [])
@@ -50,7 +50,7 @@ test('each task grade has a valid and increasing meta-skill sequence', () => {
50
50
  assert.ok(configs[2].stages.includes('任务编排'))
51
51
  assert.ok(configs[3].stages.includes('架构设计'))
52
52
  assert.ok(configs.every(config => config.transitions.some(edge => edge.requires?.includes('verified'))))
53
- assert.ok(configs.every(config => config.commit.checkpoints.includes('测试')))
53
+ assert.ok(configs.every(config => config.commit.checkpoints.includes('测试')))
54
54
  })
55
55
 
56
56
  test('adaptive task freezes a project override and its own Rules, not the bundled same-name Skill', async () => {
@@ -75,14 +75,14 @@ test('adaptive task freezes a project override and its own Rules, not the bundle
75
75
  await assert.rejects(f.call({ operation: 'load_skill', task_id: 'A-1', skill_name: 'bundled:requirements-analysis' }), /exact source:name/)
76
76
  })
77
77
 
78
- test('init_project proposes project-specific Skills and binds reviewed resources', async () => {
78
+ test('init_project proposes project-specific Skills and binds reviewed resources', async () => {
79
79
  const f = fixture()
80
- const inventory = JSON.parse(await f.call({ operation: 'init_project', phase: 'inspect' }))
81
- const projectMap = inventory.suggestions.find(entry => entry.name === 'adaptive-project-project-map')
82
- assert.ok(projectMap)
83
- assert.match(projectMap.why, /entire repository/)
84
- assert.match(projectMap.why, /current feature request/)
85
- assert.match(inventory.caution, /repository-wide/)
80
+ const inventory = JSON.parse(await f.call({ operation: 'init_project', phase: 'inspect' }))
81
+ const projectMap = inventory.suggestions.find(entry => entry.name === 'adaptive-project-project-map')
82
+ assert.ok(projectMap)
83
+ assert.match(projectMap.why, /entire repository/)
84
+ assert.match(projectMap.why, /current feature request/)
85
+ assert.match(inventory.caution, /repository-wide/)
86
86
  assert.ok(inventory.manifests.some(entry => entry.facts.includes('vue=^2.7.0')))
87
87
  const resources = [
88
88
  { kind: 'rule', name: 'component-boundary', content: 'Keep component boundary calls behind the existing service interface.' },
@@ -90,50 +90,50 @@ test('init_project proposes project-specific Skills and binds reviewed resources
90
90
  content: 'The root package.json declares Vue 2.7; inspect src for the actual component layout.',
91
91
  meta_skills: ['requirements-analysis', 'code-development'], rules: ['component-boundary'] },
92
92
  ]
93
- const proposed = JSON.parse(await f.call({ operation: 'init_project', phase: 'propose', resources }))
94
- assert.match(proposed.instruction, /whole repository/)
95
- assert.match(proposed.instruction, /current-feature details/)
93
+ const proposed = JSON.parse(await f.call({ operation: 'init_project', phase: 'propose', resources }))
94
+ assert.match(proposed.instruction, /whole repository/)
95
+ assert.match(proposed.instruction, /current-feature details/)
96
96
  assert.equal(f.files.has(join(f.cwd, '.dsh/meta.json')), false)
97
97
  await f.call({ operation: 'init_project', phase: 'apply', resources, expected_hash: proposed.expected_hash,
98
98
  existing_hash: proposed.existing_hash })
99
99
  const meta = JSON.parse(f.files.get(join(f.cwd, '.dsh/meta.json')))
100
100
  assert.deepEqual(meta.meta_bindings['code-development'], ['adaptive-project-project-map'])
101
- assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/profile.json')))
102
- })
103
-
104
- test('task-plan item headings preserve the full implementation sequence', async () => {
105
- assert.deepEqual(plannedItemIds('## Steps\n- I1 data model\n- I2 service\n### I3 UI\n- I2 service'), ['I1', 'I2', 'I3'])
106
- const f = fixture()
107
- await f.call({ operation: 'create', task_id: 'PLAN-1', title: 'Cross-module change', branch: 'feature',
108
- complexity: 'high', complexity_reason: 'Backend then frontend dependency' })
109
- const state = f.state('PLAN-1')
110
- state.stage = '任务编排'
111
- state.artifacts['task-plan'] = { steps: '- I1 model\n- I2 service', dependencies: 'I2 after I1', handoffs: 'Model to service' }
112
- state.items = [{ id: 'I2', title: 'service', status: 'todo' }]
113
- f.files.set(join(f.cwd, '.dsh/task-PLAN-1.json'), JSON.stringify(state))
114
- const status = JSON.parse(await f.call({ operation: 'status', task_id: 'PLAN-1' }))
115
- assert.match(status.task_plan_item_blockers[0], /I1/)
116
- await assert.rejects(f.call({ operation: 'advance', task_id: 'PLAN-1', target_stage: '代码开发' }), /I1/)
117
- })
118
-
101
+ assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/profile.json')))
102
+ })
103
+
104
+ test('task-plan item headings preserve the full implementation sequence', async () => {
105
+ assert.deepEqual(plannedItemIds('## Steps\n- I1 data model\n- I2 service\n### I3 UI\n- I2 service'), ['I1', 'I2', 'I3'])
106
+ const f = fixture()
107
+ await f.call({ operation: 'create', task_id: 'PLAN-1', title: 'Cross-module change', branch: 'feature',
108
+ complexity: 'high', complexity_reason: 'Backend then frontend dependency' })
109
+ const state = f.state('PLAN-1')
110
+ state.stage = '任务编排'
111
+ state.artifacts['task-plan'] = { steps: '- I1 model\n- I2 service', dependencies: 'I2 after I1', handoffs: 'Model to service' }
112
+ state.items = [{ id: 'I2', title: 'service', status: 'todo' }]
113
+ f.files.set(join(f.cwd, '.dsh/task-PLAN-1.json'), JSON.stringify(state))
114
+ const status = JSON.parse(await f.call({ operation: 'status', task_id: 'PLAN-1' }))
115
+ assert.match(status.task_plan_item_blockers[0], /I1/)
116
+ await assert.rejects(f.call({ operation: 'advance', task_id: 'PLAN-1', target_stage: '代码开发' }), /I1/)
117
+ })
118
+
119
119
  test('init_project rejects a feature-only project map that omits discovered modules', async () => {
120
- const f = fixture({
121
- 'backend/pom.xml': '<project><properties><java.version>8</java.version></properties></project>',
122
- 'frontend/package.json': '{"dependencies":{"vue":"^2.7.0"}}',
123
- })
124
- const draft = [{ kind: 'skill', name: 'adaptive-project-project-map',
125
- description: 'Reusable repository map', meta_skills: ['requirements-analysis', 'code-development'],
126
- content: '# Feature implementation\nThe root package.json and backend contain the feature API.' }]
127
- await assert.rejects(f.call({ operation: 'init_project', phase: 'propose', resources: draft }), /frontend/)
128
- draft[0].content = '# Repository map\nRoot package.json, backend/pom.xml and frontend/package.json define separate modules.'
129
- const proposed = JSON.parse(await f.call({ operation: 'init_project', phase: 'propose', resources: draft }))
130
- assert.deepEqual(proposed.project_map_coverage, ['backend', 'frontend', 'package.json'])
131
- await f.call({ operation: 'init_project', phase: 'apply', resources: draft,
132
- expected_hash: proposed.expected_hash, existing_hash: proposed.existing_hash })
133
- assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/SKILL.md')))
134
- })
120
+ const f = fixture({
121
+ 'backend/pom.xml': '<project><properties><java.version>8</java.version></properties></project>',
122
+ 'frontend/package.json': '{"dependencies":{"vue":"^2.7.0"}}',
123
+ })
124
+ const draft = [{ kind: 'skill', name: 'adaptive-project-project-map',
125
+ description: 'Reusable repository map', meta_skills: ['requirements-analysis', 'code-development'],
126
+ content: '# Feature implementation\nThe root package.json and backend contain the feature API.' }]
127
+ await assert.rejects(f.call({ operation: 'init_project', phase: 'propose', resources: draft }), /frontend/)
128
+ draft[0].content = '# Repository map\nRoot package.json, backend/pom.xml and frontend/package.json define separate modules.'
129
+ const proposed = JSON.parse(await f.call({ operation: 'init_project', phase: 'propose', resources: draft }))
130
+ assert.deepEqual(proposed.project_map_coverage, ['backend', 'frontend', 'package.json'])
131
+ await f.call({ operation: 'init_project', phase: 'apply', resources: draft,
132
+ expected_hash: proposed.expected_hash, existing_hash: proposed.existing_hash })
133
+ assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/SKILL.md')))
134
+ })
135
135
 
136
- test('workbench adaptive settings persist meta bindings with a version check', async () => {
136
+ test('workbench adaptive settings persist meta bindings with a version check', async () => {
137
137
  const f = fixture()
138
138
  const receiver = { authorizedPath: async path => path, fs: () => f.fs,
139
139
  listSkills: async () => ({ skills: [{ name: 'adaptive-project-project-map', source: 'project' }] }),
@@ -149,7 +149,7 @@ test('workbench adaptive settings persist meta bindings with a version check', a
149
149
  { path: f.cwd, config, expected_hash: before.hash }), /已被其他会话修改/)
150
150
  })
151
151
 
152
- test('Sonar audit reads the exact CI analysis gate and filters new-code medium/high findings', async () => {
152
+ test('Sonar audit reads the exact CI analysis gate and filters new-code medium/high findings', async () => {
153
153
  const calls = []
154
154
  const fetcher = async (url, options) => {
155
155
  calls.push({ url: String(url), authorization: options.headers.Authorization })
@@ -171,115 +171,115 @@ test('Sonar audit reads the exact CI analysis gate and filters new-code medium/h
171
171
  assert.ok(calls[2].url.includes('inNewCodePeriod=true'))
172
172
  assert.ok(calls.every(call => call.authorization === 'Bearer secret-token'))
173
173
  assert.equal(JSON.stringify(audit).includes('secret-token'), false)
174
- })
175
-
176
- test('IDE-style local Sonar settings save and move only that task commit after review', async () => {
177
- const sonar = { enabled: true, host_url: 'http://sonar.example', project_key: 'project',
178
- mode: 'branch', source: 'ide-local', reference_branch: 'main', token_env: 'SONAR_TOKEN' }
179
- const f = fixture()
180
- const receiver = { authorizedPath: async path => path, fs: () => f.fs,
181
- listSkills: async () => ({ skills: [] }), readAdaptive: Controller.prototype.readAdaptive }
182
- const before = await Controller.prototype.readAdaptive.call(receiver, f.cwd)
183
- const saved = await Controller.prototype.writeAdaptive.call(receiver,
184
- { path: f.cwd, config: { sonar }, expected_hash: before.hash })
185
- assert.equal(saved.ok, true)
186
- await f.call({ operation: 'create', task_id: 'L-1', title: 'local review', branch: 'feature',
187
- complexity: 'low', complexity_reason: 'Small isolated change', files: ['app.java'] })
188
- const state = f.state('L-1')
189
- assert.equal(state.sonar_policy.source, 'ide-local')
190
- assert.deepEqual(state.flow.config.commit.checkpoints, ['代码审核'])
191
- })
192
-
193
- test('local Sonar audit ignores failing old-code gate conditions', async () => {
194
- const policy = { enabled: true, host_url: 'https://sonar.example', project_key: 'project', mode: 'branch',
195
- token_env: 'SONAR_TOKEN', source: 'local', reference_branch: 'main' }
196
- const fetcher = async url => ({ ok: true, json: async () => String(url).includes('/ce/task')
197
- ? { task: { status: 'SUCCESS', analysisId: 'AN-local', componentKey: 'project', branch: 'feature' } }
198
- : String(url).includes('/qualitygates/project_status')
199
- ? { projectStatus: { status: 'ERROR', conditions: [
200
- { metricKey: 'bugs', status: 'ERROR' }, { metricKey: 'new_coverage', status: 'OK' }] } }
201
- : { paging: { total: 0 }, issues: [] } })
202
- const audit = await inspectSonar(policy, 'token', 'CE-local-123', 'feature', 'scope', fetcher, true)
203
- assert.equal(audit.gate, 'OK')
204
- assert.equal(audit.server_gate, 'ERROR')
205
- assert.deepEqual(audit.blocking, [])
206
- const newFailure = await inspectSonar(policy, 'token', 'CE-local-123', 'feature', 'scope',
207
- async url => String(url).includes('/qualitygates/project_status')
208
- ? { ok: true, json: async () => ({ projectStatus: { status: 'ERROR', conditions: [
209
- { metricKey: 'new_coverage', status: 'ERROR' }] } }) }
210
- : fetcher(url), true)
211
- assert.equal(newFailure.gate, 'ERROR')
212
- })
213
-
214
- test('local scanner configuration rejects shell operators before receiving the Token', () => {
215
- assert.equal(validLocalScanCommand('mvn org.sonarsource.scanner.maven:sonar-maven-plugin:5.5.0.6356:sonar -DskipTests'), true)
216
- assert.equal(validLocalScanCommand('mvn sonar:sonar; curl http://example'), false)
217
- assert.equal(validLocalScanCommand('mvn sonar:sonar -Dsonar.token=secret'), false)
218
- assert.equal(validLocalScanCommand('curl http://example'), false)
219
- })
220
-
221
- test('Community Sonar refuses a local branch scan before uploading any analysis', async () => {
222
- const policy = { enabled: true, host_url: 'https://sonar.example', project_key: 'project', mode: 'branch',
223
- token_env: 'SONAR_TOKEN', source: 'local', reference_branch: 'main' }
224
- const calls = []
225
- await assert.rejects(assertLocalSonarReady(policy, 'token', async url => {
226
- calls.push(String(url))
227
- return { ok: true, json: async () => ({ edition: 'community' }) }
228
- }), /Community Build supports only the main analysis branch/u)
229
- assert.equal(calls.length, 1)
230
- assert.match(calls[0], /navigation\/global/u)
231
- })
232
-
233
- test('sonar_check runs a local scan on the recorded commit without pushing or exposing Token', async () => {
234
- const calls = []
235
- const shell = { resolve: request => request, run: async spec => {
236
- calls.push(spec)
237
- if (spec.command === 'git rev-parse HEAD') return { exitCode: 0, stdout: { text: 'abcdef1234567890\n' } }
238
- if (spec.command === 'git branch --show-current') return { exitCode: 0, stdout: { text: 'feature\n' } }
239
- if (spec.command === 'git status --porcelain --untracked-files=all') return { exitCode: 0, stdout: { text: '' } }
240
- const metadata = /-Dsonar\.scanner\.metadataFilePath='([^']+)'/u.exec(spec.command)?.[1]
241
- assert.ok(metadata)
242
- writeFileSync(metadata, 'ceTaskId=CE-local-456\n')
243
- return { exitCode: 0 }
244
- } }
245
- const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true, host_url: 'https://sonar.example',
246
- project_key: 'project', mode: 'branch', source: 'local', reference_branch: 'main' } }) }, { shell })
247
- await f.call({ operation: 'create', task_id: 'LOCAL-1', title: 'change', branch: 'feature', complexity: 'low',
248
- complexity_reason: 'One local change', files: ['app.js'], items: [{ id: 'one', title: 'change', status: 'done' }] })
249
- const state = f.state('LOCAL-1')
250
- state.stage = '代码审核'
251
- state.verification = { passed: true, evidence: ['verified'] }
252
- state.commits = [{ label: 'TASK', hash: 'abcdef1234567890' }]
253
- f.files.set(join(f.cwd, '.dsh/task-LOCAL-1.json'), JSON.stringify(state))
254
- const oldFetch = globalThis.fetch
255
- const oldToken = process.env.SONAR_TOKEN
256
- process.env.SONAR_TOKEN = 'private-token'
257
- globalThis.fetch = async url => ({ ok: true, json: async () => String(url).includes('/navigation/global')
258
- ? { edition: 'developer' }
259
- : String(url).includes('/project_branches/list') ? { branches: [{ name: 'main' }] }
260
- : String(url).includes('/ce/task')
261
- ? { task: { status: 'SUCCESS', analysisId: 'AN-local', componentKey: 'project', branch: 'feature' } }
262
- : String(url).includes('/qualitygates/project_status')
263
- ? { projectStatus: { status: 'ERROR', conditions: [{ metricKey: 'bugs', status: 'ERROR' }] } }
264
- : { paging: { total: 0 }, issues: [] } })
265
- try {
266
- const result = JSON.parse(await f.call({ operation: 'sonar_check', task_id: 'LOCAL-1' }))
267
- assert.equal(result.gate, 'OK')
268
- assert.equal(result.server_gate, 'ERROR')
269
- assert.equal(calls.length, 4)
270
- assert.equal(calls[3].env.SONAR_TOKEN, 'private-token')
271
- assert.equal(calls[3].command.includes('private-token'), false)
272
- assert.match(calls[3].command, /sonar\.newCode\.referenceBranch='main'/u)
273
- assert.equal(JSON.stringify(f.state('LOCAL-1')).includes('private-token'), false)
274
- assert.equal(calls.some(call => /git push/u.test(call.command)), false)
275
- } finally {
276
- globalThis.fetch = oldFetch
277
- if (oldToken === undefined) delete process.env.SONAR_TOKEN
278
- else process.env.SONAR_TOKEN = oldToken
279
- }
280
- })
174
+ })
175
+
176
+ test('IDE-style local Sonar settings save and move only that task commit after review', async () => {
177
+ const sonar = { enabled: true, host_url: 'http://sonar.example', project_key: 'project',
178
+ mode: 'branch', source: 'ide-local', reference_branch: 'main', token_env: 'SONAR_TOKEN' }
179
+ const f = fixture()
180
+ const receiver = { authorizedPath: async path => path, fs: () => f.fs,
181
+ listSkills: async () => ({ skills: [] }), readAdaptive: Controller.prototype.readAdaptive }
182
+ const before = await Controller.prototype.readAdaptive.call(receiver, f.cwd)
183
+ const saved = await Controller.prototype.writeAdaptive.call(receiver,
184
+ { path: f.cwd, config: { sonar }, expected_hash: before.hash })
185
+ assert.equal(saved.ok, true)
186
+ await f.call({ operation: 'create', task_id: 'L-1', title: 'local review', branch: 'feature',
187
+ complexity: 'low', complexity_reason: 'Small isolated change', files: ['app.java'] })
188
+ const state = f.state('L-1')
189
+ assert.equal(state.sonar_policy.source, 'ide-local')
190
+ assert.deepEqual(state.flow.config.commit.checkpoints, ['代码审核'])
191
+ })
281
192
 
282
- test('review-only Sonar gate blocks pass until the task records CI analysis of new code', async () => {
193
+ test('local Sonar audit ignores failing old-code gate conditions', async () => {
194
+ const policy = { enabled: true, host_url: 'https://sonar.example', project_key: 'project', mode: 'branch',
195
+ token_env: 'SONAR_TOKEN', source: 'local', reference_branch: 'main' }
196
+ const fetcher = async url => ({ ok: true, json: async () => String(url).includes('/ce/task')
197
+ ? { task: { status: 'SUCCESS', analysisId: 'AN-local', componentKey: 'project', branch: 'feature' } }
198
+ : String(url).includes('/qualitygates/project_status')
199
+ ? { projectStatus: { status: 'ERROR', conditions: [
200
+ { metricKey: 'bugs', status: 'ERROR' }, { metricKey: 'new_coverage', status: 'OK' }] } }
201
+ : { paging: { total: 0 }, issues: [] } })
202
+ const audit = await inspectSonar(policy, 'token', 'CE-local-123', 'feature', 'scope', fetcher, true)
203
+ assert.equal(audit.gate, 'OK')
204
+ assert.equal(audit.server_gate, 'ERROR')
205
+ assert.deepEqual(audit.blocking, [])
206
+ const newFailure = await inspectSonar(policy, 'token', 'CE-local-123', 'feature', 'scope',
207
+ async url => String(url).includes('/qualitygates/project_status')
208
+ ? { ok: true, json: async () => ({ projectStatus: { status: 'ERROR', conditions: [
209
+ { metricKey: 'new_coverage', status: 'ERROR' }] } }) }
210
+ : fetcher(url), true)
211
+ assert.equal(newFailure.gate, 'ERROR')
212
+ })
213
+
214
+ test('local scanner configuration rejects shell operators before receiving the Token', () => {
215
+ assert.equal(validLocalScanCommand('mvn org.sonarsource.scanner.maven:sonar-maven-plugin:5.5.0.6356:sonar -DskipTests'), true)
216
+ assert.equal(validLocalScanCommand('mvn sonar:sonar; curl http://example'), false)
217
+ assert.equal(validLocalScanCommand('mvn sonar:sonar -Dsonar.token=secret'), false)
218
+ assert.equal(validLocalScanCommand('curl http://example'), false)
219
+ })
220
+
221
+ test('Community Sonar refuses a local branch scan before uploading any analysis', async () => {
222
+ const policy = { enabled: true, host_url: 'https://sonar.example', project_key: 'project', mode: 'branch',
223
+ token_env: 'SONAR_TOKEN', source: 'local', reference_branch: 'main' }
224
+ const calls = []
225
+ await assert.rejects(assertLocalSonarReady(policy, 'token', async url => {
226
+ calls.push(String(url))
227
+ return { ok: true, json: async () => ({ edition: 'community' }) }
228
+ }), /Community Build supports only the main analysis branch/u)
229
+ assert.equal(calls.length, 1)
230
+ assert.match(calls[0], /navigation\/global/u)
231
+ })
232
+
233
+ test('sonar_check runs a local scan on the recorded commit without pushing or exposing Token', async () => {
234
+ const calls = []
235
+ const shell = { resolve: request => request, run: async spec => {
236
+ calls.push(spec)
237
+ if (spec.command === 'git rev-parse HEAD') return { exitCode: 0, stdout: { text: 'abcdef1234567890\n' } }
238
+ if (spec.command === 'git branch --show-current') return { exitCode: 0, stdout: { text: 'feature\n' } }
239
+ if (spec.command === 'git status --porcelain --untracked-files=all') return { exitCode: 0, stdout: { text: '' } }
240
+ const metadata = /-Dsonar\.scanner\.metadataFilePath='([^']+)'/u.exec(spec.command)?.[1]
241
+ assert.ok(metadata)
242
+ writeFileSync(metadata, 'ceTaskId=CE-local-456\n')
243
+ return { exitCode: 0 }
244
+ } }
245
+ const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true, host_url: 'https://sonar.example',
246
+ project_key: 'project', mode: 'branch', source: 'local', reference_branch: 'main' } }) }, { shell })
247
+ await f.call({ operation: 'create', task_id: 'LOCAL-1', title: 'change', branch: 'feature', complexity: 'low',
248
+ complexity_reason: 'One local change', files: ['app.js'], items: [{ id: 'one', title: 'change', status: 'done' }] })
249
+ const state = f.state('LOCAL-1')
250
+ state.stage = '代码审核'
251
+ state.verification = { passed: true, evidence: ['verified'] }
252
+ state.commits = [{ label: 'TASK', hash: 'abcdef1234567890' }]
253
+ f.files.set(join(f.cwd, '.dsh/task-LOCAL-1.json'), JSON.stringify(state))
254
+ const oldFetch = globalThis.fetch
255
+ const oldToken = process.env.SONAR_TOKEN
256
+ process.env.SONAR_TOKEN = 'private-token'
257
+ globalThis.fetch = async url => ({ ok: true, json: async () => String(url).includes('/navigation/global')
258
+ ? { edition: 'developer' }
259
+ : String(url).includes('/project_branches/list') ? { branches: [{ name: 'main' }] }
260
+ : String(url).includes('/ce/task')
261
+ ? { task: { status: 'SUCCESS', analysisId: 'AN-local', componentKey: 'project', branch: 'feature' } }
262
+ : String(url).includes('/qualitygates/project_status')
263
+ ? { projectStatus: { status: 'ERROR', conditions: [{ metricKey: 'bugs', status: 'ERROR' }] } }
264
+ : { paging: { total: 0 }, issues: [] } })
265
+ try {
266
+ const result = JSON.parse(await f.call({ operation: 'sonar_check', task_id: 'LOCAL-1' }))
267
+ assert.equal(result.gate, 'OK')
268
+ assert.equal(result.server_gate, 'ERROR')
269
+ assert.equal(calls.length, 4)
270
+ assert.equal(calls[3].env.SONAR_TOKEN, 'private-token')
271
+ assert.equal(calls[3].command.includes('private-token'), false)
272
+ assert.match(calls[3].command, /sonar\.newCode\.referenceBranch='main'/u)
273
+ assert.equal(JSON.stringify(f.state('LOCAL-1')).includes('private-token'), false)
274
+ assert.equal(calls.some(call => /git push/u.test(call.command)), false)
275
+ } finally {
276
+ globalThis.fetch = oldFetch
277
+ if (oldToken === undefined) delete process.env.SONAR_TOKEN
278
+ else process.env.SONAR_TOKEN = oldToken
279
+ }
280
+ })
281
+
282
+ test('review-only Sonar gate blocks pass until the task records CI analysis of new code', async () => {
283
283
  const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true, host_url: 'https://sonar.example',
284
284
  project_key: 'project', mode: 'branch', token_env: 'SONAR_TOKEN' } }) })
285
285
  await f.call({ operation: 'create', task_id: 'S-1', title: 'change', branch: 'feature', complexity: 'low',
@@ -295,16 +295,16 @@ test('review-only Sonar gate blocks pass until the task records CI analysis of n
295
295
  process.env.SONAR_TOKEN = 'secret-token'
296
296
  globalThis.fetch = async url => ({ ok: true, json: async () => String(url).includes('/ce/task')
297
297
  ? { task: { status: 'SUCCESS', analysisId: 'AN-2', componentKey: 'project' } }
298
- : String(url).includes('/qualitygates/project_status') ? { projectStatus: { status: 'ERROR',
299
- conditions: [{ metricKey: 'bugs', status: 'ERROR' }, { metricKey: 'new_coverage', status: 'OK' }] } }
298
+ : String(url).includes('/qualitygates/project_status') ? { projectStatus: { status: 'ERROR',
299
+ conditions: [{ metricKey: 'bugs', status: 'ERROR' }, { metricKey: 'new_coverage', status: 'OK' }] } }
300
300
  : { paging: { total: 0 }, issues: [] } })
301
301
  try {
302
- const check = JSON.parse(await f.call({ operation: 'sonar_check', task_id: 'S-1', ce_task_id: 'CE-123456' }))
303
- assert.equal(check.gate, 'OK')
304
- assert.equal(check.server_gate, 'ERROR')
305
- assert.match(check.report_path, /^\.dsh\/reviews\/S-1\//)
306
- assert.match(f.files.get(join(f.cwd, check.report_path)), /SonarQube 代码审核/)
307
- assert.equal(f.state('S-1').sonar_audit.report_path, check.report_path)
302
+ const check = JSON.parse(await f.call({ operation: 'sonar_check', task_id: 'S-1', ce_task_id: 'CE-123456' }))
303
+ assert.equal(check.gate, 'OK')
304
+ assert.equal(check.server_gate, 'ERROR')
305
+ assert.match(check.report_path, /^\.dsh\/reviews\/S-1\//)
306
+ assert.match(f.files.get(join(f.cwd, check.report_path)), /SonarQube 代码审核/)
307
+ assert.equal(f.state('S-1').sonar_audit.report_path, check.report_path)
308
308
  await f.call({ operation: 'review', task_id: 'S-1', outcome: 'pass' })
309
309
  assert.equal(f.state('S-1').review.outcome, 'pass')
310
310
  } finally {
@@ -314,7 +314,7 @@ test('review-only Sonar gate blocks pass until the task records CI analysis of n
314
314
  }
315
315
  })
316
316
 
317
- test('a failed Sonar finding becomes a reviewed project Rule attached to the coding Skill', async () => {
317
+ test('a failed Sonar finding becomes a reviewed project Rule attached to the coding Skill', async () => {
318
318
  const f = fixture()
319
319
  await f.call({ operation: 'create', task_id: 'L-1', title: 'change', branch: 'feature', complexity: 'low',
320
320
  complexity_reason: 'One local change', files: ['app.js'] })
@@ -324,10 +324,10 @@ test('a failed Sonar finding becomes a reviewed project Rule attached to the cod
324
324
  state.sonar_audit = { ce_task_id: 'CE-123456', analysis_id: 'AN-9', gate: 'ERROR', checked_at: 'now',
325
325
  scope_hash: 'old', findings: [finding], blocking: [finding], target: 'feature' }
326
326
  f.files.set(join(f.cwd, '.dsh/task-L-1.json'), JSON.stringify(state))
327
- await f.call({ operation: 'revise', task_id: 'L-1', target_stage: '代码开发',
328
- revision_kind: 'defect', revision_reason: 'Fix the SonarQube issue' })
329
- assert.equal(f.state('L-1').sonar_history[0].blocking[0].key, 'ISSUE-9')
330
- assert.equal(JSON.parse(await f.call({ operation: 'status', task_id: 'L-1' })).rule_learning_candidates[0].rule, 'java:S999')
327
+ await f.call({ operation: 'revise', task_id: 'L-1', target_stage: '代码开发',
328
+ revision_kind: 'defect', revision_reason: 'Fix the SonarQube issue' })
329
+ assert.equal(f.state('L-1').sonar_history[0].blocking[0].key, 'ISSUE-9')
330
+ assert.equal(JSON.parse(await f.call({ operation: 'status', task_id: 'L-1' })).rule_learning_candidates[0].rule, 'java:S999')
331
331
  const args = { operation: 'learn_rule', task_id: 'L-1', issue_key: 'ISSUE-9', skill_name: 'bundled:code-development',
332
332
  rule_name: 'handle-null-boundary', learning_reason: 'This boundary appears in several service methods',
333
333
  content: 'At a service boundary, check for null before dereferencing. Show the failing call and a guarded call in the review example.' }
@@ -336,7 +336,86 @@ test('a failed Sonar finding becomes a reviewed project Rule attached to the cod
336
336
  await f.call({ ...args, phase: 'apply', expected_hash: proposal.expected_hash })
337
337
  assert.match(f.files.get(join(f.cwd, '.dsh/rules/handle-null-boundary.md')), /java:S999/)
338
338
  assert.ok(f.files.has(join(f.cwd, '.dsh/skills/code-development/SKILL.md')))
339
- assert.deepEqual(JSON.parse(f.files.get(join(f.cwd, '.dsh/skills/code-development/profile.json'))).rules,
340
- [{ source: 'project', name: 'handle-null-boundary' }])
341
- assert.deepEqual(JSON.parse(await f.call({ operation: 'status', task_id: 'L-1' })).rule_learning_candidates, [])
339
+ assert.deepEqual(JSON.parse(f.files.get(join(f.cwd, '.dsh/skills/code-development/profile.json'))).rules,
340
+ [{ source: 'project', name: 'handle-null-boundary' }])
341
+ assert.deepEqual(JSON.parse(await f.call({ operation: 'status', task_id: 'L-1' })).rule_learning_candidates, [])
342
+ })
343
+
344
+ test('workbench initializes reviewed project resources directly and preserves Sonar settings', async () => {
345
+ const f = fixture({
346
+ 'backend/pom.xml': '<project><properties><java.version>8</java.version></properties></project>',
347
+ 'frontend/package.json': '{"dependencies":{"vue":"^2.7.0"}}',
348
+ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: false, project_key: 'keep-me' } }),
349
+ })
350
+ const resources = [
351
+ { kind: 'rule', name: 'backend-convention', content: 'Use the backend module Maven Java 8 build for backend changes.' },
352
+ { kind: 'skill', name: 'adaptive-project-project-map', description: 'Reusable repository map',
353
+ content: 'Root package.json, backend/pom.xml and frontend/package.json define the backend and frontend modules.',
354
+ meta_skills: ['requirements-analysis', 'code-development'], rules: ['backend-convention'] },
355
+ ]
356
+ const receiver = {
357
+ authorizedPath: async path => path, fs: () => f.fs, listSkills: async () => ({ skills: [] }),
358
+ projectInitRoot: Controller.prototype.projectInitRoot,
359
+ previewProjectInit: Controller.prototype.previewProjectInit,
360
+ ctx: { get(name) {
361
+ if (name === 'llm') return { async *stream() { yield { type: 'text-delta', text: JSON.stringify({ resources }) } } }
362
+ if (name === 'agentDefaultModel') return { currentSelection: () => ({ provider: 'test', model: 'test' }) }
363
+ } },
364
+ }
365
+ const draft = await Controller.prototype.generateProjectInit.call(receiver, { path: f.cwd })
366
+ assert.deepEqual(draft.project_map_coverage, ['backend', 'frontend', 'package.json'])
367
+ assert.equal(f.files.has(join(f.cwd, '.dsh/rules/backend-convention.md')), false)
368
+ await assert.rejects(Controller.prototype.applyProjectInit.call(receiver,
369
+ { path: f.cwd, resources: draft.resources, expected_hash: 'stale', existing_hash: draft.existing_hash }), /改变/)
370
+ const applied = await Controller.prototype.applyProjectInit.call(receiver,
371
+ { path: f.cwd, resources: draft.resources, expected_hash: draft.expected_hash, existing_hash: draft.existing_hash })
372
+ assert.equal(applied.ok, true)
373
+ assert.equal(f.files.get(join(f.cwd, '.dsh/meta.json')).includes('keep-me'), true)
374
+ assert.equal(JSON.parse(f.files.get(join(f.cwd, '.dsh/meta.json'))).meta_bindings['code-development'][0],
375
+ 'adaptive-project-project-map')
376
+ assert.ok(f.files.has(join(f.cwd, '.dsh/skills/adaptive-project-project-map/SKILL.md')))
377
+ await assert.rejects(Controller.prototype.previewProjectInit.call(receiver,
378
+ { path: f.cwd, resources }), /不能覆盖/)
379
+ })
380
+
381
+ test('a local false positive needs human approval, keeps the raw gate, and expires after code changes', async () => {
382
+ let allow = false
383
+ const approvals = []
384
+ const shell = { resolve: request => request, execute: async () => ({ result: async () => ({
385
+ exitCode: 0, timedOut: false, aborted: false, stdout: { text: 'checks passed' }, stderr: { text: '' },
386
+ }) }) }
387
+ const f = fixture({ '.dsh/meta.json': JSON.stringify({ sonar: { enabled: true,
388
+ host_url: 'https://sonar.example', project_key: 'project', mode: 'branch', source: 'ide-local',
389
+ reference_branch: 'main' } }) }, { shell,
390
+ approval: { request: async request => { approvals.push(request); return allow ? 'allowed-once' : 'rejected' } } })
391
+ await f.call({ operation: 'create', task_id: 'FP-1', title: 'one local finding', branch: 'feature',
392
+ complexity: 'low', complexity_reason: 'Local correction', files: ['app.js'] })
393
+ await f.call({ operation: 'verify', task_id: 'FP-1', command: 'check' })
394
+ const state = f.state('FP-1')
395
+ state.stage = '代码审核'
396
+ const finding = { key: 'local-fp-1', rule: 'mycompany-java:AvoidObjectCreationInLoop',
397
+ message: 'Avoid allocation', severity: 'MAJOR', file: 'app.js', line: 3 }
398
+ state.sonar_audit = { ce_task_id: 'local', analysis_id: 'LOCAL-1', gate: 'ERROR',
399
+ checked_at: '2026-10-05T00:00:00.000Z', scope_hash: state.verification.receipt.scope_hash,
400
+ findings: [finding], blocking: [finding], target: 'main', scanned_files: ['app.js'],
401
+ report_path: '.dsh/reviews/FP-1/local.md' }
402
+ f.files.set(join(f.cwd, '.dsh/reviews/FP-1/local.md'), 'original report')
403
+ f.files.set(join(f.cwd, '.dsh/task-FP-1.json'), JSON.stringify(state))
404
+ const args = { operation: 'sonar_disposition', task_id: 'FP-1', issue_key: finding.key,
405
+ disposition_reason: '每次循环需要独立实体对象,复用同一个实例会让多条授权记录互相覆盖,属于规则误报。',
406
+ evidence: ['授权实体逐行添加到集合,复用对象会导致所有行指向末次值'] }
407
+ await assert.rejects(f.call(args), /not approved/)
408
+ assert.equal(f.state('FP-1').sonar_audit.dispositions, undefined)
409
+ allow = true
410
+ const result = JSON.parse(await f.call(args))
411
+ assert.equal(result.raw_gate, 'ERROR')
412
+ assert.equal(result.review_gate, 'OK')
413
+ assert.equal(f.state('FP-1').sonar_audit.dispositions.length, 1)
414
+ assert.deepEqual(JSON.parse(await f.call({ operation: 'status', task_id: 'FP-1' })).rule_learning_candidates, [])
415
+ assert.match(approvals.at(-1).reason, /local-fp-1/)
416
+ assert.match(f.files.get(join(f.cwd, state.sonar_audit.report_path)), /逐项误报复核/)
417
+ await f.call({ operation: 'review', task_id: 'FP-1', outcome: 'pass' })
418
+ await assert.rejects(f.call({ operation: 'learn_rule', task_id: 'FP-1', issue_key: finding.key }), /blocking issue_key/)
419
+ f.files.set(join(f.cwd, 'app.js'), 'new source')
420
+ await assert.rejects(f.call({ operation: 'review', task_id: 'FP-1', outcome: 'pass' }), /stale/)
342
421
  })
@@ -7,11 +7,11 @@
7
7
  */
8
8
  import test from 'node:test'
9
9
  import assert from 'node:assert/strict'
10
- import { loadedSkills, needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
10
+ import { loadedSkills, needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
11
11
  import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
12
12
 
13
13
  /** A session that has loaded the named skills. */
14
- function sessionWith(names) {
14
+ function sessionWith(names) {
15
15
  const events = []
16
16
  for (const name of names) {
17
17
  const callId = 'c' + events.length
@@ -19,20 +19,20 @@ function sessionWith(names) {
19
19
  events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: false }] } } })
20
20
  }
21
21
  return { id: 's', snapshotEvents: () => events }
22
- }
23
-
24
- test('evidence: DSH 0.2 tool messages prove only successful task-scoped skill loads', () => {
25
- const events = [
26
- { type: 'tool/call', data: { name: 'dev_task', callId: 'ok', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-project-map' }) } },
27
- { type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'ok', isError: false, content: [{ type: 'text', text: 'loaded' }] } } },
28
- { type: 'tool/call', data: { name: 'dev_task', callId: 'bad', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-tech-stack' }) } },
29
- { type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'bad', isError: true, content: [{ type: 'text', text: 'failed' }] } } },
30
- ]
31
- const loaded = loadedSkills({ snapshotEvents: () => events })
32
- assert.equal(loaded.get('Q-1#project:qms-project-map'), 'ok')
33
- assert.equal(loaded.has('Q-1#project:qms-tech-stack'), false)
34
- assert.equal(loaded.has('Q-2#project:qms-project-map'), false)
35
- })
22
+ }
23
+
24
+ test('evidence: DSH 0.2 tool messages prove only successful task-scoped skill loads', () => {
25
+ const events = [
26
+ { type: 'tool/call', data: { name: 'dev_task', callId: 'ok', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-project-map' }) } },
27
+ { type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'ok', isError: false, content: [{ type: 'text', text: 'loaded' }] } } },
28
+ { type: 'tool/call', data: { name: 'dev_task', callId: 'bad', arguments: JSON.stringify({ operation: 'load_skill', task_id: 'Q-1', skill_name: 'project:qms-tech-stack' }) } },
29
+ { type: 'tool/result', data: { message: { role: 'tool', toolCallId: 'bad', isError: true, content: [{ type: 'text', text: 'failed' }] } } },
30
+ ]
31
+ const loaded = loadedSkills({ snapshotEvents: () => events })
32
+ assert.equal(loaded.get('Q-1#project:qms-project-map'), 'ok')
33
+ assert.equal(loaded.has('Q-1#project:qms-tech-stack'), false)
34
+ assert.equal(loaded.has('Q-2#project:qms-project-map'), false)
35
+ })
36
36
 
37
37
  /** A standard-flow config with one extra skill on 开发 carrying the given evidence. */
38
38
  function withEvidence(evidence) {