@godv61/dsh-task-engine 0.25.0 → 0.26.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 (58) hide show
  1. package/.acceptance.mjs +150 -150
  2. package/.assessment-batch1.mjs +20 -20
  3. package/.enforce-test.mjs +138 -138
  4. package/.evidence-test.mjs +118 -119
  5. package/.filter-test.mjs +92 -92
  6. package/.hook-test.mjs +195 -195
  7. package/.p0-test.mjs +59 -59
  8. package/.preset-test.mjs +160 -160
  9. package/.revision-test.mjs +30 -16
  10. package/.roundtrip-test.mjs +178 -63
  11. package/.workflow-test.mjs +86 -0
  12. package/README.md +8 -8
  13. package/cordis.patch.yml +10 -10
  14. package/defaults/eng.json +70 -22
  15. package/docs/BRIEF-FOR-REVIEW.md +162 -162
  16. package/docs/CHANGELOG.md +18 -0
  17. package/docs/README.md +40 -40
  18. package/docs/configuration.md +14 -9
  19. package/docs/development.md +1 -1
  20. package/docs/faq.md +4 -4
  21. package/docs/listing/godv61__dsh-task-engine.yml +5 -5
  22. package/docs/listing/submission.md +83 -83
  23. package/docs/manual.html +32 -27
  24. package/docs/releases/0.23.2.md +21 -21
  25. package/docs/roadmap.md +34 -34
  26. 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
  27. 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
  28. package/docs/testing/0.23.2//346/265/213/350/257/225/346/212/245/345/221/212.md +65 -65
  29. package/hooks/commit-msg +70 -1
  30. package/lib/client.js +870 -571
  31. package/lib/client.js.map +3 -3
  32. package/lib/controller.d.ts +2 -1
  33. package/lib/controller.js +6 -3
  34. package/lib/controller.js.map +1 -1
  35. package/lib/dev-task.js +83 -15
  36. package/lib/dev-task.js.map +1 -1
  37. package/lib/engine.d.ts +15 -0
  38. package/lib/engine.js +29 -5
  39. package/lib/engine.js.map +1 -1
  40. package/lib/hook.js +3 -0
  41. package/lib/hook.js.map +1 -1
  42. package/lib/skill-audit.js +3 -3
  43. package/lib/skill-audit.js.map +1 -1
  44. package/lib/workflows.d.ts +4 -1
  45. package/lib/workflows.js +90 -2
  46. package/lib/workflows.js.map +1 -1
  47. package/package.json +1 -1
  48. package/preset/agent.cordis.yml +22 -22
  49. package/preset/enable.mjs +87 -87
  50. package/preset/persona.md +5 -5
  51. package/preset/preset.yml +1 -1
  52. package/rules/coding-conventions.md +6 -6
  53. package/rules/commit-conventions.md +6 -6
  54. package/rules/security-redlines.md +5 -5
  55. package/scripts/verify-dsh-compat.mjs +144 -144
  56. package/skills/code-implement/SKILL.md +1 -1
  57. package/skills/code-verify/SKILL.md +3 -3
  58. package/skills/eng-delivery/SKILL.md +10 -31
@@ -1,119 +1,118 @@
1
- /**
2
- * Evidence kinds: a skill is judged against the proof it actually declares.
3
- *
4
- * Demanding a "real validation command" from every non-core skill forced an
5
- * irrelevant command from a requirement or design skill, whose output is a
6
- * document. The evidence existed — in another form.
7
- */
8
- import test from 'node:test'
9
- import assert from 'node:assert/strict'
10
- import { needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
11
- import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
12
-
13
- /** A session that has loaded the named skills. */
14
- function sessionWith(names) {
15
- const events = []
16
- for (const name of names) {
17
- const callId = 'c' + events.length
18
- events.push({ type: 'tool/call', data: { name: 'skill', callId, arguments: JSON.stringify({ name }) } })
19
- events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: false }] } } })
20
- }
21
- return { id: 's', snapshotEvents: () => events }
22
- }
23
-
24
- /** A standard-flow config with one extra skill on 开发 carrying the given evidence. */
25
- function withEvidence(evidence) {
26
- const adopted = adoptRecommendation('standard')
27
- const skills = [...adopted.stage_bindings['开发'].skills, { skill: { source: 'project', name: 'doc-skill' }, rules: [], ...(evidence !== undefined ? { evidence } : {}) }]
28
- return resolveFlow('standard', { ...adopted, stage_bindings: { ...adopted.stage_bindings, '开发': { skills } } }).config
29
- }
30
-
31
- /** A task at 开发 with everything else satisfied. */
32
- function state(extra = {}) {
33
- return {
34
- schema: 1, id: 'E-1', title: 't', branch: 'main', work_size: 'standard', risk_level: 'standard',
35
- stage: '开发', execution_version: 1,
36
- requirement_confirmed: true, solution_confirmed: true,
37
- items: [], artifacts: {}, files: [], commits: [],
38
- verification: { passed: false, evidence: [] },
39
- review: { outcome: 'pending' },
40
- ...extra,
41
- }
42
- }
43
-
44
- test('evidence: needsSkillReceipt honours a declared non-command kind', () => {
45
- // Core skills keep their exemption; a declared kind overrides the default.
46
- assert.equal(needsSkillReceipt('code-implement'), false, 'a core skill owes no separate receipt')
47
- assert.equal(needsSkillReceipt('doc-skill'), true, 'an undeclared extra skill defaults to a command receipt')
48
- assert.equal(needsSkillReceipt('doc-skill', 'command'), true)
49
- assert.equal(needsSkillReceipt('doc-skill', 'artifact'), false)
50
- assert.equal(needsSkillReceipt('doc-skill', 'review'), false)
51
- assert.equal(needsSkillReceipt('doc-skill', 'manual'), false)
52
- assert.equal(needsSkillReceipt('doc-skill', 'none'), false)
53
- })
54
-
55
- test('evidence: a document-producing skill is satisfied by a recorded artifact', () => {
56
- // The blocker is the point: without an artifact it blocks, and with one it does
57
- // not — so the declared kind is genuinely enforced rather than merely accepted.
58
- //
59
- // Mounted on 设计, which the standard flow declares an artifact for. A stage with
60
- // no declared artifact has nothing to satisfy artifact evidence against, and the
61
- // audit deliberately stays silent there rather than inventing a requirement.
62
- const adopted = adoptRecommendation('standard')
63
- const config = resolveFlow('standard', {
64
- ...adopted,
65
- stage_bindings: {
66
- ...adopted.stage_bindings,
67
- '设计': { skills: [{ skill: { source: 'project', name: 'doc-skill' }, rules: [], evidence: 'artifact' }] },
68
- },
69
- }).config
70
- const session = sessionWith(['doc-skill'])
71
- const atDesign = extra => ({ ...state({ stage: '设计', ...extra }) })
72
-
73
- const missing = skillBlockers(atDesign(), config, session)
74
- assert.ok(missing.some(b => b.includes('artifact evidence')),
75
- `an artifact-evidence skill must ask for an artifact; got ${JSON.stringify(missing)}`)
76
- assert.ok(!missing.some(b => b.includes('validation command')),
77
- 'it must NOT ask for a shell command')
78
-
79
- // Record what the stage declares and the blocker clears.
80
- const declared = config.artifacts.find(artifact => artifact.stage === '设计')
81
- assert.ok(declared !== undefined, 'the standard flow declares an artifact at 设计')
82
- const cleared = skillBlockers(atDesign({ artifacts: { [declared.id]: { fields: { approach: 'x' } } } }), config, session)
83
- assert.deepEqual(cleared.filter(b => b.includes('doc-skill')), [],
84
- 'a recorded artifact satisfies artifact evidence')
85
- })
86
-
87
- test('evidence: a judging skill is satisfied by a passing review', () => {
88
- const config = withEvidence('review')
89
- const session = sessionWith(['code-implement', 'doc-skill'])
90
- assert.ok(skillBlockers(state(), config, session).some(b => b.includes('review evidence')),
91
- 'no review recorded means the review evidence is absent')
92
- assert.deepEqual(
93
- skillBlockers(state({ review: { outcome: 'pass' } }), config, session).filter(b => b.includes('doc-skill')), [],
94
- 'a passing review satisfies it')
95
- })
96
-
97
- test('evidence: manual evidence needs an explicit statement, none needs nothing', () => {
98
- const manual = withEvidence('manual')
99
- const session = sessionWith(['code-implement', 'doc-skill'])
100
- assert.ok(skillBlockers(state(), manual, session).some(b => b.includes('manual evidence')),
101
- 'manual evidence is absent until something is recorded')
102
- const recorded = state({ skill_results: { '开发': { 'doc-skill': { evidence: ['reviewed by the lead'] } } } })
103
- assert.deepEqual(skillBlockers(recorded, manual, session).filter(b => b.includes('doc-skill')), [],
104
- 'a recorded statement satisfies manual evidence')
105
-
106
- const none = withEvidence('none')
107
- assert.deepEqual(skillBlockers(state(), none, session).filter(b => b.includes('doc-skill')), [],
108
- 'an advisory skill needs no proof beyond being loaded')
109
- })
110
-
111
- test('evidence: an undeclared extra skill still owes a command receipt', () => {
112
- // The historical behaviour must survive: a config that says nothing keeps getting
113
- // the command requirement, so existing setups are unaffected.
114
- const config = withEvidence(undefined)
115
- const session = sessionWith(['code-implement', 'doc-skill'])
116
- const blockers = skillBlockers(state(), config, session)
117
- assert.ok(blockers.some(b => b.includes('validation command')),
118
- `an undeclared skill must still owe a command receipt; got ${JSON.stringify(blockers)}`)
119
- })
1
+ /**
2
+ * Evidence kinds: a skill is judged against the proof it actually declares.
3
+ *
4
+ * Demanding a "real validation command" from every non-core skill forced an
5
+ * irrelevant command from a requirement or design skill, whose output is a
6
+ * document. The evidence existed — in another form.
7
+ */
8
+ import test from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import { needsSkillReceipt, skillBlockers } from './lib/skill-audit.js'
11
+ import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
12
+
13
+ /** A session that has loaded the named skills. */
14
+ function sessionWith(names) {
15
+ const events = []
16
+ for (const name of names) {
17
+ const callId = 'c' + events.length
18
+ events.push({ type: 'tool/call', data: { name: 'skill', callId, arguments: JSON.stringify({ name }) } })
19
+ events.push({ type: 'tool/result', data: { message: { content: [{ type: 'tool-result', toolCallId: callId, isError: false }] } } })
20
+ }
21
+ return { id: 's', snapshotEvents: () => events }
22
+ }
23
+
24
+ /** A standard-flow config with one extra skill on 开发 carrying the given evidence. */
25
+ function withEvidence(evidence) {
26
+ const adopted = adoptRecommendation('standard')
27
+ const skills = [...adopted.stage_bindings['开发'].skills, { skill: { source: 'project', name: 'doc-skill' }, rules: [], ...(evidence !== undefined ? { evidence } : {}) }]
28
+ return resolveFlow('standard', { ...adopted, stage_bindings: { ...adopted.stage_bindings, '开发': { skills } } }).config
29
+ }
30
+
31
+ /** A task at 开发 with everything else satisfied. */
32
+ function state(extra = {}) {
33
+ return {
34
+ schema: 1, id: 'E-1', title: 't', branch: 'main', work_size: 'standard', risk_level: 'standard',
35
+ stage: '开发', execution_version: 1,
36
+ requirement_confirmed: true, solution_confirmed: true,
37
+ items: [], artifacts: {}, files: [], commits: [],
38
+ verification: { passed: false, evidence: [] },
39
+ review: { outcome: 'pending' },
40
+ ...extra,
41
+ }
42
+ }
43
+
44
+ test('evidence: needsSkillReceipt honours a declared non-command kind', () => {
45
+ // Core skills keep their exemption; a declared kind overrides the default.
46
+ assert.equal(needsSkillReceipt('code-implement'), false, 'a core skill owes no separate receipt')
47
+ assert.equal(needsSkillReceipt('doc-skill'), true, 'an undeclared extra skill defaults to a command receipt')
48
+ assert.equal(needsSkillReceipt('doc-skill', 'command'), true)
49
+ assert.equal(needsSkillReceipt('doc-skill', 'artifact'), false)
50
+ assert.equal(needsSkillReceipt('doc-skill', 'review'), false)
51
+ assert.equal(needsSkillReceipt('doc-skill', 'manual'), false)
52
+ assert.equal(needsSkillReceipt('doc-skill', 'none'), false)
53
+ })
54
+
55
+ test('evidence: a document-producing skill is satisfied by a recorded artifact', () => {
56
+ // The blocker is the point: without an artifact it blocks, and with one it does
57
+ // not — so the declared kind is genuinely enforced rather than merely accepted.
58
+ //
59
+ // Mounted on 设计, which the adopted setup declares an artifact for. Missing
60
+ // declarations and incomplete fields must both remain blockers.
61
+ const adopted = adoptRecommendation('standard')
62
+ const config = resolveFlow('standard', {
63
+ ...adopted,
64
+ stage_bindings: {
65
+ ...adopted.stage_bindings,
66
+ '设计': { skills: [{ skill: { source: 'project', name: 'doc-skill' }, rules: [], evidence: 'artifact' }] },
67
+ },
68
+ }).config
69
+ const session = sessionWith(['doc-skill'])
70
+ const atDesign = extra => ({ ...state({ stage: '设计', ...extra }) })
71
+
72
+ const missing = skillBlockers(atDesign(), config, session)
73
+ assert.ok(missing.some(b => b.includes('artifact evidence')),
74
+ `an artifact-evidence skill must ask for an artifact; got ${JSON.stringify(missing)}`)
75
+ assert.ok(!missing.some(b => b.includes('validation command')),
76
+ 'it must NOT ask for a shell command')
77
+
78
+ // Record what the stage declares and the blocker clears.
79
+ const declared = config.artifacts.find(artifact => artifact.stage === '设计')
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)
82
+ assert.deepEqual(cleared.filter(b => b.includes('doc-skill')), [],
83
+ 'a recorded artifact satisfies artifact evidence')
84
+ })
85
+
86
+ test('evidence: a judging skill is satisfied by a passing review', () => {
87
+ const config = withEvidence('review')
88
+ const session = sessionWith(['code-implement', 'doc-skill'])
89
+ assert.ok(skillBlockers(state(), config, session).some(b => b.includes('review evidence')),
90
+ 'no review recorded means the review evidence is absent')
91
+ assert.deepEqual(
92
+ skillBlockers(state({ review: { outcome: 'pass' } }), config, session).filter(b => b.includes('doc-skill')), [],
93
+ 'a passing review satisfies it')
94
+ })
95
+
96
+ test('evidence: manual evidence needs an explicit statement, none needs nothing', () => {
97
+ const manual = withEvidence('manual')
98
+ const session = sessionWith(['code-implement', 'doc-skill'])
99
+ assert.ok(skillBlockers(state(), manual, session).some(b => b.includes('manual evidence')),
100
+ 'manual evidence is absent until something is recorded')
101
+ const recorded = state({ skill_results: { '开发': { 'doc-skill': { evidence: ['reviewed by the lead'], approved: true } } } })
102
+ assert.deepEqual(skillBlockers(recorded, manual, session).filter(b => b.includes('doc-skill')), [],
103
+ 'a recorded statement satisfies manual evidence')
104
+
105
+ const none = withEvidence('none')
106
+ assert.deepEqual(skillBlockers(state(), none, session).filter(b => b.includes('doc-skill')), [],
107
+ 'an advisory skill needs no proof beyond being loaded')
108
+ })
109
+
110
+ test('evidence: an undeclared extra skill still owes a command receipt', () => {
111
+ // The historical behaviour must survive: a config that says nothing keeps getting
112
+ // the command requirement, so existing setups are unaffected.
113
+ const config = withEvidence(undefined)
114
+ const session = sessionWith(['code-implement', 'doc-skill'])
115
+ const blockers = skillBlockers(state(), config, session)
116
+ assert.ok(blockers.some(b => b.includes('validation command')),
117
+ `an undeclared skill must still owe a command receipt; got ${JSON.stringify(blockers)}`)
118
+ })
package/.filter-test.mjs CHANGED
@@ -1,93 +1,93 @@
1
- /**
2
- * Catalog filtering: search and filters must narrow the list without ever hiding
3
- * something that is currently in force.
4
- *
5
- * The panel shows the configuration; a filter that hid a bound skill would make the
6
- * panel disagree with the config it is displaying, which is worse than a long list.
7
- */
8
- import test from 'node:test'
9
- import assert from 'node:assert/strict'
10
- import { bindingsForStage, formatResourceRef } from './lib/engine.js'
11
- import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
12
-
13
- /**
14
- * The filter the panel applies, lifted so it can be tested without a DOM.
15
- *
16
- * Mirrors `visibleSkills` in TaskEngineSection: an entry that is bound or shipped
17
- * always matches; otherwise the query must appear in its name, description or layer.
18
- */
19
- function visibleSkills(catalog, binding, presetSkills, query, onlySelected) {
20
- const boundKeys = new Set((binding?.skills ?? []).map(entry => formatResourceRef(entry.skill)))
21
- const presetKeys = new Set(presetSkills.map(entry => formatResourceRef(entry.skill)))
22
- const needle = query.trim().toLowerCase()
23
- return catalog.filter(entry => {
24
- const raw = formatResourceRef(entry.ref)
25
- const inForce = boundKeys.has(raw) || presetKeys.has(raw)
26
- if (onlySelected && !inForce) return false
27
- if (needle === '') return true
28
- return inForce
29
- || entry.name.toLowerCase().includes(needle)
30
- || entry.description.toLowerCase().includes(needle)
31
- || entry.sourceLabel.toLowerCase().includes(needle)
32
- })
33
- }
34
-
35
- const catalog = [
36
- { ref: { source: 'bundled', name: 'code-implement' }, name: 'code-implement', description: '实现', sourceLabel: '内置' },
37
- { ref: { source: 'bundled', name: 'code-review' }, name: 'code-review', description: '评审变更', sourceLabel: '内置' },
38
- { ref: { source: 'project', name: 'api-audit' }, name: 'api-audit', description: '审计接口契约', sourceLabel: '项目' },
39
- { ref: { source: 'user', name: 'doc-writer' }, name: 'doc-writer', description: '写文档', sourceLabel: '用户' },
40
- ]
41
- const binding = { skills: [{ skill: { source: 'project', name: 'api-audit' }, rules: [] }] }
42
-
43
- test('filter: no query shows everything', () => {
44
- assert.equal(visibleSkills(catalog, binding, [], '', false).length, 4)
45
- })
46
-
47
- test('filter: the query narrows by name, description and layer', () => {
48
- // `api-audit` is bound in this fixture, so it stays visible whatever the query —
49
- // that rule is asserted separately, and these expectations include it.
50
- const names = (...args) => visibleSkills(...args).map(e => e.name).sort()
51
- assert.deepEqual(names(catalog, binding, [], 'writer', false), ['api-audit', 'doc-writer'],
52
- 'name matches')
53
- assert.deepEqual(names(catalog, binding, [], '写文档', false), ['api-audit', 'doc-writer'],
54
- 'description matches')
55
- assert.deepEqual(names(catalog, binding, [], '用户', false), ['api-audit', 'doc-writer'],
56
- 'the layer label matches, so a user can list one layer')
57
- assert.deepEqual(names(catalog, binding, [], '审计', false), ['api-audit'],
58
- 'a bound skill matching by its own description is shown')
59
- assert.deepEqual(names(catalog, binding, [], 'nothing-matches-this', false), ['api-audit'],
60
- 'a query matching nothing still shows the skill that is in force')
61
- })
62
-
63
- test('filter: a BOUND skill is never hidden by the query', () => {
64
- // The panel shows the configuration. A search that hid a skill currently in force
65
- // would make the panel disagree with the config it is displaying.
66
- const shown = visibleSkills(catalog, binding, [], 'doc', false)
67
- assert.ok(shown.some(e => e.name === 'api-audit'),
68
- 'the bound skill stays visible even though it does not match the query')
69
- assert.ok(shown.some(e => e.name === 'doc-writer'), 'and the matching one is shown too')
70
- })
71
-
72
- test('filter: only-selected keeps bound and preset skills, drops the rest', () => {
73
- const shown = visibleSkills(catalog, binding, [{ skill: { source: 'bundled', name: 'code-review' } }], '', true)
74
- const names = shown.map(e => e.name).sort()
75
- assert.deepEqual(names, ['api-audit', 'code-review'],
76
- 'a bound skill and a preset-shipped one are both in force; the other two are not')
77
- })
78
-
79
- test('filter: only-selected combined with a query still keeps what is in force', () => {
80
- const shown = visibleSkills(catalog, binding, [], 'doc', true)
81
- assert.ok(shown.some(e => e.name === 'api-audit'),
82
- 'the filter must not drop a bound skill even when a query is active')
83
- assert.ok(!shown.some(e => e.name === 'code-implement'), 'an unbound non-matching skill is dropped')
84
- })
85
-
86
- test('filter: the shipped catalog is small enough that the default view shows it all', () => {
87
- // Sanity: the initial view of a fresh project is the shipped set, unfiltered.
88
- const adopted = adoptRecommendation('standard')
89
- const resolved = resolveFlow('standard', adopted).config
90
- const binding = bindingsForStage('开发', resolved)
91
- assert.ok(binding !== undefined, 'the adopted config binds 开发')
92
- assert.ok((binding.skills ?? []).length > 0)
1
+ /**
2
+ * Catalog filtering: search and filters must narrow the list without ever hiding
3
+ * something that is currently in force.
4
+ *
5
+ * The panel shows the configuration; a filter that hid a bound skill would make the
6
+ * panel disagree with the config it is displaying, which is worse than a long list.
7
+ */
8
+ import test from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import { bindingsForStage, formatResourceRef } from './lib/engine.js'
11
+ import { adoptRecommendation, resolveFlow } from './lib/workflows.js'
12
+
13
+ /**
14
+ * The filter the panel applies, lifted so it can be tested without a DOM.
15
+ *
16
+ * Mirrors `visibleSkills` in TaskEngineSection: an entry that is bound or shipped
17
+ * always matches; otherwise the query must appear in its name, description or layer.
18
+ */
19
+ function visibleSkills(catalog, binding, presetSkills, query, onlySelected) {
20
+ const boundKeys = new Set((binding?.skills ?? []).map(entry => formatResourceRef(entry.skill)))
21
+ const presetKeys = new Set(presetSkills.map(entry => formatResourceRef(entry.skill)))
22
+ const needle = query.trim().toLowerCase()
23
+ return catalog.filter(entry => {
24
+ const raw = formatResourceRef(entry.ref)
25
+ const inForce = boundKeys.has(raw) || presetKeys.has(raw)
26
+ if (onlySelected && !inForce) return false
27
+ if (needle === '') return true
28
+ return inForce
29
+ || entry.name.toLowerCase().includes(needle)
30
+ || entry.description.toLowerCase().includes(needle)
31
+ || entry.sourceLabel.toLowerCase().includes(needle)
32
+ })
33
+ }
34
+
35
+ const catalog = [
36
+ { ref: { source: 'bundled', name: 'code-implement' }, name: 'code-implement', description: '实现', sourceLabel: '内置' },
37
+ { ref: { source: 'bundled', name: 'code-review' }, name: 'code-review', description: '评审变更', sourceLabel: '内置' },
38
+ { ref: { source: 'project', name: 'api-audit' }, name: 'api-audit', description: '审计接口契约', sourceLabel: '项目' },
39
+ { ref: { source: 'user', name: 'doc-writer' }, name: 'doc-writer', description: '写文档', sourceLabel: '用户' },
40
+ ]
41
+ const binding = { skills: [{ skill: { source: 'project', name: 'api-audit' }, rules: [] }] }
42
+
43
+ test('filter: no query shows everything', () => {
44
+ assert.equal(visibleSkills(catalog, binding, [], '', false).length, 4)
45
+ })
46
+
47
+ test('filter: the query narrows by name, description and layer', () => {
48
+ // `api-audit` is bound in this fixture, so it stays visible whatever the query —
49
+ // that rule is asserted separately, and these expectations include it.
50
+ const names = (...args) => visibleSkills(...args).map(e => e.name).sort()
51
+ assert.deepEqual(names(catalog, binding, [], 'writer', false), ['api-audit', 'doc-writer'],
52
+ 'name matches')
53
+ assert.deepEqual(names(catalog, binding, [], '写文档', false), ['api-audit', 'doc-writer'],
54
+ 'description matches')
55
+ assert.deepEqual(names(catalog, binding, [], '用户', false), ['api-audit', 'doc-writer'],
56
+ 'the layer label matches, so a user can list one layer')
57
+ assert.deepEqual(names(catalog, binding, [], '审计', false), ['api-audit'],
58
+ 'a bound skill matching by its own description is shown')
59
+ assert.deepEqual(names(catalog, binding, [], 'nothing-matches-this', false), ['api-audit'],
60
+ 'a query matching nothing still shows the skill that is in force')
61
+ })
62
+
63
+ test('filter: a BOUND skill is never hidden by the query', () => {
64
+ // The panel shows the configuration. A search that hid a skill currently in force
65
+ // would make the panel disagree with the config it is displaying.
66
+ const shown = visibleSkills(catalog, binding, [], 'doc', false)
67
+ assert.ok(shown.some(e => e.name === 'api-audit'),
68
+ 'the bound skill stays visible even though it does not match the query')
69
+ assert.ok(shown.some(e => e.name === 'doc-writer'), 'and the matching one is shown too')
70
+ })
71
+
72
+ test('filter: only-selected keeps bound and preset skills, drops the rest', () => {
73
+ const shown = visibleSkills(catalog, binding, [{ skill: { source: 'bundled', name: 'code-review' } }], '', true)
74
+ const names = shown.map(e => e.name).sort()
75
+ assert.deepEqual(names, ['api-audit', 'code-review'],
76
+ 'a bound skill and a preset-shipped one are both in force; the other two are not')
77
+ })
78
+
79
+ test('filter: only-selected combined with a query still keeps what is in force', () => {
80
+ const shown = visibleSkills(catalog, binding, [], 'doc', true)
81
+ assert.ok(shown.some(e => e.name === 'api-audit'),
82
+ 'the filter must not drop a bound skill even when a query is active')
83
+ assert.ok(!shown.some(e => e.name === 'code-implement'), 'an unbound non-matching skill is dropped')
84
+ })
85
+
86
+ test('filter: the shipped catalog is small enough that the default view shows it all', () => {
87
+ // Sanity: the initial view of a fresh project is the shipped set, unfiltered.
88
+ const adopted = adoptRecommendation('standard')
89
+ const resolved = resolveFlow('standard', adopted).config
90
+ const binding = bindingsForStage('开发', resolved)
91
+ assert.ok(binding !== undefined, 'the adopted config binds 开发')
92
+ assert.ok((binding.skills ?? []).length > 0)
93
93
  })