tackle-harness 0.0.2
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/LICENSE +21 -0
- package/README.en.md +259 -0
- package/README.md +261 -0
- package/bin/tackle.js +150 -0
- package/package.json +29 -0
- package/plugins/contracts/plugin-interface.js +244 -0
- package/plugins/core/hook-skill-gate/index.js +437 -0
- package/plugins/core/hook-skill-gate/plugin.json +12 -0
- package/plugins/core/provider-memory-store/index.js +403 -0
- package/plugins/core/provider-memory-store/plugin.json +9 -0
- package/plugins/core/provider-role-registry/index.js +477 -0
- package/plugins/core/provider-role-registry/plugin.json +9 -0
- package/plugins/core/provider-state-store/index.js +244 -0
- package/plugins/core/provider-state-store/plugin.json +9 -0
- package/plugins/core/skill-agent-dispatcher/plugin.json +13 -0
- package/plugins/core/skill-agent-dispatcher/skill.md +912 -0
- package/plugins/core/skill-batch-task-creator/plugin.json +13 -0
- package/plugins/core/skill-batch-task-creator/skill.md +616 -0
- package/plugins/core/skill-checklist/plugin.json +10 -0
- package/plugins/core/skill-checklist/skill.md +115 -0
- package/plugins/core/skill-completion-report/plugin.json +10 -0
- package/plugins/core/skill-completion-report/skill.md +331 -0
- package/plugins/core/skill-experience-logger/plugin.json +10 -0
- package/plugins/core/skill-experience-logger/skill.md +235 -0
- package/plugins/core/skill-human-checkpoint/plugin.json +10 -0
- package/plugins/core/skill-human-checkpoint/skill.md +194 -0
- package/plugins/core/skill-progress-tracker/plugin.json +10 -0
- package/plugins/core/skill-progress-tracker/skill.md +204 -0
- package/plugins/core/skill-role-manager/plugin.json +10 -0
- package/plugins/core/skill-role-manager/skill.md +252 -0
- package/plugins/core/skill-split-work-package/plugin.json +13 -0
- package/plugins/core/skill-split-work-package/skill.md +446 -0
- package/plugins/core/skill-task-creator/plugin.json +13 -0
- package/plugins/core/skill-task-creator/skill.md +744 -0
- package/plugins/core/skill-team-cleanup/plugin.json +10 -0
- package/plugins/core/skill-team-cleanup/skill.md +266 -0
- package/plugins/core/skill-workflow-orchestrator/plugin.json +13 -0
- package/plugins/core/skill-workflow-orchestrator/skill.md +274 -0
- package/plugins/core/validator-doc-sync/index.js +248 -0
- package/plugins/core/validator-doc-sync/plugin.json +9 -0
- package/plugins/core/validator-work-package/index.js +300 -0
- package/plugins/core/validator-work-package/plugin.json +9 -0
- package/plugins/plugin-registry.json +118 -0
- package/plugins/runtime/config-manager.js +306 -0
- package/plugins/runtime/event-bus.js +187 -0
- package/plugins/runtime/harness-build.js +1019 -0
- package/plugins/runtime/logger.js +174 -0
- package/plugins/runtime/plugin-loader.js +339 -0
- package/plugins/runtime/state-store.js +277 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validator-doc-sync - 文档同步验证器
|
|
3
|
+
*
|
|
4
|
+
* 检测 task.md 与 docs/wp/ 目录之间的不一致:
|
|
5
|
+
* - task.md 中列出但 docs/wp/ 中缺少的 WP 文档
|
|
6
|
+
* - docs/wp/ 中存在但 task.md 未列出的 WP 文档
|
|
7
|
+
* - 状态不一致(task.md 标记完成但 WP 文档内容为空等)
|
|
8
|
+
*
|
|
9
|
+
* 使用方式:
|
|
10
|
+
* var Validator = require('./index.js');
|
|
11
|
+
* var v = new Validator({ projectRoot: '/path/to/project' });
|
|
12
|
+
* var result = v.validate(); // { valid, errors, warnings }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
var fs = require('fs');
|
|
18
|
+
var path = require('path');
|
|
19
|
+
var { ValidatorPlugin } = require('../../contracts/plugin-interface');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* WP 编号正则:匹配 task.md 表格行中的 WP-NNN
|
|
23
|
+
*/
|
|
24
|
+
var WP_TABLE_ROW_RE = /^\|\s*(WP-\d+)\s*\|/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 状态标记正则
|
|
28
|
+
*/
|
|
29
|
+
var STATUS_COMPLETE_RE = /✅\s*完成/;
|
|
30
|
+
var STATUS_IN_PROGRESS_RE = /🔄\s*进行中/;
|
|
31
|
+
var STATUS_PENDING_RE = /📋\s*待开始/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 从 task.md 提取状态标记
|
|
35
|
+
*/
|
|
36
|
+
function parseStatus(statusCell) {
|
|
37
|
+
if (STATUS_COMPLETE_RE.test(statusCell)) return 'completed';
|
|
38
|
+
if (STATUS_IN_PROGRESS_RE.test(statusCell)) return 'in-progress';
|
|
39
|
+
if (STATUS_PENDING_RE.test(statusCell)) return 'pending';
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 检查文件内容是否实质为空
|
|
45
|
+
* 除去空白、标题行、YAML front matter 后内容极少视为空
|
|
46
|
+
*/
|
|
47
|
+
function isContentEmpty(content) {
|
|
48
|
+
if (!content) return true;
|
|
49
|
+
|
|
50
|
+
// 去除 YAML front matter
|
|
51
|
+
var body = content.replace(/^---[\s\S]*?---\n*/, '');
|
|
52
|
+
// 去除 markdown 标题和分隔线
|
|
53
|
+
body = body.replace(/^#{1,6}\s+.*$/gm, '');
|
|
54
|
+
body = body.replace(/^---+$/gm, '');
|
|
55
|
+
body = body.replace(/^>.*$/gm, '');
|
|
56
|
+
// 去除纯空白行
|
|
57
|
+
var lines = body.split('\n').filter(function (line) {
|
|
58
|
+
return line.trim().length > 0;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return lines.length < 3;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* DocSyncValidator
|
|
66
|
+
*/
|
|
67
|
+
class DocSyncValidator extends ValidatorPlugin {
|
|
68
|
+
/**
|
|
69
|
+
* @param {object} [options]
|
|
70
|
+
* @param {string} [options.projectRoot] - 项目根目录,默认 process.cwd()
|
|
71
|
+
*/
|
|
72
|
+
constructor(options) {
|
|
73
|
+
super();
|
|
74
|
+
options = options || {};
|
|
75
|
+
|
|
76
|
+
this.name = 'validator-doc-sync';
|
|
77
|
+
this.version = '1.0.0';
|
|
78
|
+
this.description = '文档同步验证器';
|
|
79
|
+
this.blocking = false;
|
|
80
|
+
|
|
81
|
+
/** @type {string} */
|
|
82
|
+
this._projectRoot = options.projectRoot || process.cwd();
|
|
83
|
+
/** @type {string} */
|
|
84
|
+
this._taskMdPath = path.join(this._projectRoot, 'task.md');
|
|
85
|
+
/** @type {string} */
|
|
86
|
+
this._docsWpDir = path.join(this._projectRoot, 'docs', 'wp');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 解析 task.md,提取所有 WP 条目及其状态
|
|
91
|
+
* @returns {Array<{id: string, status: string, rawStatus: string}>}
|
|
92
|
+
*/
|
|
93
|
+
parseTaskMd() {
|
|
94
|
+
var content = this._readFile(this._taskMdPath);
|
|
95
|
+
if (content === null) return [];
|
|
96
|
+
|
|
97
|
+
var entries = [];
|
|
98
|
+
var lines = content.split('\n');
|
|
99
|
+
|
|
100
|
+
for (var i = 0; i < lines.length; i++) {
|
|
101
|
+
var match = WP_TABLE_ROW_RE.exec(lines[i]);
|
|
102
|
+
if (match) {
|
|
103
|
+
// 拆分表格单元格
|
|
104
|
+
var cells = lines[i].split('|').filter(function (c) { return c.trim().length > 0; });
|
|
105
|
+
var rawStatus = cells.length >= 2 ? cells[1].trim() : '';
|
|
106
|
+
// cells[0] 是 WP ID, cells[1] 是标题, cells[2] 是状态
|
|
107
|
+
// 实际格式:| WP-NNN | 标题 | 状态 | ...
|
|
108
|
+
rawStatus = cells.length >= 3 ? cells[2].trim() : (cells.length >= 2 ? cells[1].trim() : '');
|
|
109
|
+
|
|
110
|
+
entries.push({
|
|
111
|
+
id: match[1],
|
|
112
|
+
status: parseStatus(rawStatus),
|
|
113
|
+
rawStatus: rawStatus,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return entries;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 扫描 docs/wp/ 目录,返回存在的 WP 文件列表
|
|
123
|
+
* @returns {string[]} WP ID 列表,如 ['WP-001', 'WP-002']
|
|
124
|
+
*/
|
|
125
|
+
scanDocsWp() {
|
|
126
|
+
var entries = [];
|
|
127
|
+
var files = this._readdir(this._docsWpDir);
|
|
128
|
+
|
|
129
|
+
for (var i = 0; i < files.length; i++) {
|
|
130
|
+
var match = /^(WP-\d+)\.md$/i.exec(files[i]);
|
|
131
|
+
if (match) {
|
|
132
|
+
entries.push(match[1].toUpperCase());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 执行文档同步验证
|
|
141
|
+
* @param {object} [context] - 验证上下文(保留兼容接口)
|
|
142
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
143
|
+
*/
|
|
144
|
+
validate(context) {
|
|
145
|
+
var errors = [];
|
|
146
|
+
var warnings = [];
|
|
147
|
+
|
|
148
|
+
// 1. 检查 task.md 可读
|
|
149
|
+
var taskEntries = this.parseTaskMd();
|
|
150
|
+
if (taskEntries.length === 0) {
|
|
151
|
+
// 尝试读取原始文件判断是真的空还是读不到
|
|
152
|
+
var rawContent = this._readFile(this._taskMdPath);
|
|
153
|
+
if (rawContent === null) {
|
|
154
|
+
errors.push('task.md 文件不存在或无法读取: ' + this._taskMdPath);
|
|
155
|
+
return { valid: false, errors: errors, warnings: warnings };
|
|
156
|
+
}
|
|
157
|
+
// 文件存在但解析不出 WP 条目
|
|
158
|
+
warnings.push('task.md 中未找到工作包条目');
|
|
159
|
+
return { valid: true, errors: errors, warnings: warnings };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 2. 检查 docs/wp/ 目录可读
|
|
163
|
+
var docFiles = this.scanDocsWp();
|
|
164
|
+
|
|
165
|
+
// 3. 构建索引集合
|
|
166
|
+
var taskMap = {};
|
|
167
|
+
for (var i = 0; i < taskEntries.length; i++) {
|
|
168
|
+
taskMap[taskEntries[i].id] = taskEntries[i];
|
|
169
|
+
}
|
|
170
|
+
var docSet = {};
|
|
171
|
+
for (var j = 0; j < docFiles.length; j++) {
|
|
172
|
+
docSet[docFiles[j]] = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 4. task.md 中有但 docs/wp/ 缺少的 WP
|
|
176
|
+
for (var k = 0; k < taskEntries.length; k++) {
|
|
177
|
+
var id = taskEntries[k].id;
|
|
178
|
+
if (!docSet[id]) {
|
|
179
|
+
errors.push(id + ' 在 task.md 中列出但 docs/wp/ 中缺少对应文档');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 5. docs/wp/ 中有但 task.md 未列出的 WP
|
|
184
|
+
for (var m = 0; m < docFiles.length; m++) {
|
|
185
|
+
var docId = docFiles[m];
|
|
186
|
+
if (!taskMap[docId]) {
|
|
187
|
+
warnings.push(docId + ' 在 docs/wp/ 中存在但 task.md 未列出');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 6. 状态不一致检测:task.md 标记完成但 WP 文档内容为空
|
|
192
|
+
for (var n = 0; n < taskEntries.length; n++) {
|
|
193
|
+
var entry = taskEntries[n];
|
|
194
|
+
if (entry.status === 'completed' && docSet[entry.id]) {
|
|
195
|
+
var wpPath = path.join(this._docsWpDir, entry.id + '.md');
|
|
196
|
+
var wpContent = this._readFile(wpPath);
|
|
197
|
+
if (wpContent !== null && isContentEmpty(wpContent)) {
|
|
198
|
+
errors.push(entry.id + ' 在 task.md 中标记为已完成,但 WP 文档内容实质为空');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// task.md 标记待开始但 WP 文档内容丰富(可能忘记更新状态)
|
|
203
|
+
if (entry.status === 'pending' && docSet[entry.id]) {
|
|
204
|
+
var wpPendingPath = path.join(this._docsWpDir, entry.id + '.md');
|
|
205
|
+
var wpPendingContent = this._readFile(wpPendingPath);
|
|
206
|
+
if (wpPendingContent !== null && !isContentEmpty(wpPendingContent)) {
|
|
207
|
+
warnings.push(entry.id + ' 在 task.md 中标记为待开始,但 WP 文档已有实质内容,可能需要更新状态');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
valid: errors.length === 0,
|
|
214
|
+
errors: errors,
|
|
215
|
+
warnings: warnings,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// --- internal helpers ---
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 同步读取文件,失败返回 null
|
|
223
|
+
* @param {string} filePath
|
|
224
|
+
* @returns {string|null}
|
|
225
|
+
*/
|
|
226
|
+
_readFile(filePath) {
|
|
227
|
+
try {
|
|
228
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 同步读取目录,失败返回空数组
|
|
236
|
+
* @param {string} dirPath
|
|
237
|
+
* @returns {string[]}
|
|
238
|
+
*/
|
|
239
|
+
_readdir(dirPath) {
|
|
240
|
+
try {
|
|
241
|
+
return fs.readdirSync(dirPath);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = DocSyncValidator;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validator-work-package - 工作包文档验证器
|
|
3
|
+
*
|
|
4
|
+
* 检测 WP 文档结构缺陷和依赖有效性:
|
|
5
|
+
* - 验证必要章节(目标、验收标准、关键文件)
|
|
6
|
+
* - 验证依赖关系引用的 WP 是否存在
|
|
7
|
+
* - validate(wpPath) 验证单个 WP 文档
|
|
8
|
+
* - validateAll() 验证所有 WP 文档
|
|
9
|
+
*
|
|
10
|
+
* 使用方式:
|
|
11
|
+
* var Validator = require('./index.js');
|
|
12
|
+
* var v = new Validator({ projectRoot: '/path/to/project' });
|
|
13
|
+
* var result = v.validate('/path/to/WP-001.md');
|
|
14
|
+
* var allResult = v.validateAll();
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
var fs = require('fs');
|
|
20
|
+
var path = require('path');
|
|
21
|
+
var { ValidatorPlugin } = require('../../contracts/plugin-interface');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* WP 文档必要章节定义
|
|
25
|
+
* 每个 WP 文档应包含这些章节标题(二级标题 ## 或更高级别)
|
|
26
|
+
*/
|
|
27
|
+
var REQUIRED_SECTIONS = [
|
|
28
|
+
{ id: 'goal', patterns: [/^#{1,6}\s*目标/m, /^#{1,6}\s*Goal/im] },
|
|
29
|
+
{ id: 'acceptance', patterns: [/^#{1,6}\s*验收标准/m, /^#{1,6}\s*Acceptance/im] },
|
|
30
|
+
{ id: 'keyfiles', patterns: [/^#{1,6}\s*关键文件/m, /^#{1,6}\s*Key\s*Files/im] },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 依赖关系提取正则
|
|
35
|
+
* 匹配 "依赖: WP-001, WP-002" 或 "依赖: 无" 或 "Dependencies: ..."
|
|
36
|
+
*/
|
|
37
|
+
var DEPS_LINE_RE = /^>\s*依赖[::]\s*(.+)$/m;
|
|
38
|
+
var DEPS_LINE_ALT_RE = /^\*?\*?依赖\*?\*?[::]\s*(.+)$/m;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* WP ID 提取正则
|
|
42
|
+
*/
|
|
43
|
+
var WP_ID_RE = /WP-\d+/gi;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* WorkPackageValidator
|
|
47
|
+
*/
|
|
48
|
+
class WorkPackageValidator extends ValidatorPlugin {
|
|
49
|
+
/**
|
|
50
|
+
* @param {object} [options]
|
|
51
|
+
* @param {string} [options.projectRoot] - 项目根目录,默认 process.cwd()
|
|
52
|
+
*/
|
|
53
|
+
constructor(options) {
|
|
54
|
+
super();
|
|
55
|
+
options = options || {};
|
|
56
|
+
|
|
57
|
+
this.name = 'validator-work-package';
|
|
58
|
+
this.version = '1.0.0';
|
|
59
|
+
this.description = '工作包文档验证器';
|
|
60
|
+
this.blocking = false;
|
|
61
|
+
|
|
62
|
+
/** @type {string} */
|
|
63
|
+
this._projectRoot = options.projectRoot || process.cwd();
|
|
64
|
+
/** @type {string} */
|
|
65
|
+
this._docsWpDir = path.join(this._projectRoot, 'docs', 'wp');
|
|
66
|
+
/** @type {string|null} */
|
|
67
|
+
this._taskMdPath = path.join(this._projectRoot, 'task.md');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 获取所有已知 WP ID(从 docs/wp/ 目录和 task.md 收集)
|
|
72
|
+
* @returns {Set<string>}
|
|
73
|
+
*/
|
|
74
|
+
_getKnownWpIds() {
|
|
75
|
+
var ids = new Set();
|
|
76
|
+
|
|
77
|
+
// 从 docs/wp/ 目录
|
|
78
|
+
var files = this._readdir(this._docsWpDir);
|
|
79
|
+
for (var i = 0; i < files.length; i++) {
|
|
80
|
+
var match = /^(WP-\d+)\.md$/i.exec(files[i]);
|
|
81
|
+
if (match) {
|
|
82
|
+
ids.add(match[1].toUpperCase());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return ids;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 从 WP 文档内容中提取依赖的 WP ID
|
|
91
|
+
* @param {string} content
|
|
92
|
+
* @returns {string[]}
|
|
93
|
+
*/
|
|
94
|
+
_extractDependencies(content) {
|
|
95
|
+
var deps = [];
|
|
96
|
+
|
|
97
|
+
// 尝试匹配依赖行
|
|
98
|
+
var match = DEPS_LINE_RE.exec(content) || DEPS_LINE_ALT_RE.exec(content);
|
|
99
|
+
if (match) {
|
|
100
|
+
var depStr = match[1].trim();
|
|
101
|
+
if (depStr === '无' || depStr === '-' || depStr === 'none') {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var ids = depStr.match(WP_ID_RE);
|
|
106
|
+
if (ids) {
|
|
107
|
+
for (var i = 0; i < ids.length; i++) {
|
|
108
|
+
deps.push(ids[i].toUpperCase());
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 也检查依赖图或表格中的引用(mermaid 语法)
|
|
114
|
+
var mermaidBlock = /```mermaid[\s\S]*?```/g.exec(content);
|
|
115
|
+
if (mermaidBlock) {
|
|
116
|
+
var mermaidDeps = mermaidBlock[0].match(/WP-\d+/gi);
|
|
117
|
+
if (mermaidDeps) {
|
|
118
|
+
for (var j = 0; j < mermaidDeps.length; j++) {
|
|
119
|
+
var id = mermaidDeps[j].toUpperCase();
|
|
120
|
+
if (deps.indexOf(id) === -1) {
|
|
121
|
+
deps.push(id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return deps;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 检查文档中是否包含指定章节
|
|
132
|
+
* @param {string} content
|
|
133
|
+
* @param {{ id: string, patterns: RegExp[] }} section
|
|
134
|
+
* @returns {boolean}
|
|
135
|
+
*/
|
|
136
|
+
_hasSection(content, section) {
|
|
137
|
+
for (var i = 0; i < section.patterns.length; i++) {
|
|
138
|
+
if (section.patterns[i].test(content)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 验证单个 WP 文档
|
|
147
|
+
* @param {string} wpPath - WP 文档的完整路径,或 WP ID (如 "WP-001")
|
|
148
|
+
* @param {object} [context] - 验证上下文(保留兼容接口)
|
|
149
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
150
|
+
*/
|
|
151
|
+
validate(wpPath, context) {
|
|
152
|
+
var errors = [];
|
|
153
|
+
var warnings = [];
|
|
154
|
+
|
|
155
|
+
// 支持 WP ID 简写
|
|
156
|
+
var filePath = wpPath;
|
|
157
|
+
if (/^WP-\d+$/i.test(wpPath)) {
|
|
158
|
+
filePath = path.join(this._docsWpDir, wpPath.toUpperCase() + '.md');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
var wpId = path.basename(filePath, '.md').toUpperCase();
|
|
162
|
+
var content = this._readFile(filePath);
|
|
163
|
+
|
|
164
|
+
if (content === null) {
|
|
165
|
+
errors.push(wpId + ' 文档文件不存在或无法读取: ' + filePath);
|
|
166
|
+
return { valid: false, errors: errors, warnings: warnings };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 1. 检查必要章节
|
|
170
|
+
for (var i = 0; i < REQUIRED_SECTIONS.length; i++) {
|
|
171
|
+
var section = REQUIRED_SECTIONS[i];
|
|
172
|
+
if (!this._hasSection(content, section)) {
|
|
173
|
+
errors.push(wpId + ' 缺少必要章节: ' + section.id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. 检查依赖关系有效性
|
|
178
|
+
var deps = this._extractDependencies(content);
|
|
179
|
+
if (deps.length > 0) {
|
|
180
|
+
var knownIds = this._getKnownWpIds();
|
|
181
|
+
|
|
182
|
+
for (var j = 0; j < deps.length; j++) {
|
|
183
|
+
// 跳过自引用
|
|
184
|
+
if (deps[j] === wpId) {
|
|
185
|
+
warnings.push(wpId + ' 依赖关系中包含自引用');
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (!knownIds.has(deps[j])) {
|
|
189
|
+
errors.push(wpId + ' 引用了不存在的依赖: ' + deps[j]);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 3. 额外结构检查
|
|
195
|
+
|
|
196
|
+
// 检查是否有预估时间
|
|
197
|
+
if (!/预估时间|Estimated\s*Time/i.test(content)) {
|
|
198
|
+
warnings.push(wpId + ' 缺少预估时间章节');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 检查验收标准是否有复选框条目
|
|
202
|
+
if (/验收标准/.test(content)) {
|
|
203
|
+
var acceptanceSection = content.split(/#{1,6}\s*验收标准/)[1];
|
|
204
|
+
if (acceptanceSection) {
|
|
205
|
+
// 取到下一个章节标题之前
|
|
206
|
+
var nextSection = acceptanceSection.split(/^#{1,6}\s/m)[0];
|
|
207
|
+
if (nextSection && !/^\s*- \[[ x]\]/m.test(nextSection) && !/^\s*\[[ x]\]/m.test(nextSection)) {
|
|
208
|
+
warnings.push(wpId + ' 验收标准中未发现复选框条目,建议使用 - [ ] 格式列出验收项');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
valid: errors.length === 0,
|
|
215
|
+
errors: errors,
|
|
216
|
+
warnings: warnings,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 验证所有 WP 文档
|
|
222
|
+
* @param {object} [context] - 验证上下文(保留兼容接口)
|
|
223
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[], results: object }}
|
|
224
|
+
*/
|
|
225
|
+
validateAll(context) {
|
|
226
|
+
var allErrors = [];
|
|
227
|
+
var allWarnings = [];
|
|
228
|
+
var perFile = {};
|
|
229
|
+
|
|
230
|
+
var files = this._readdir(this._docsWpDir);
|
|
231
|
+
var wpFiles = [];
|
|
232
|
+
|
|
233
|
+
for (var i = 0; i < files.length; i++) {
|
|
234
|
+
if (/^WP-\d+\.md$/i.test(files[i])) {
|
|
235
|
+
wpFiles.push(files[i]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (wpFiles.length === 0) {
|
|
240
|
+
allWarnings.push('docs/wp/ 目录下未找到任何 WP 文档');
|
|
241
|
+
return {
|
|
242
|
+
valid: true,
|
|
243
|
+
errors: allErrors,
|
|
244
|
+
warnings: allWarnings,
|
|
245
|
+
results: perFile,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
for (var j = 0; j < wpFiles.length; j++) {
|
|
250
|
+
var filePath = path.join(this._docsWpDir, wpFiles[j]);
|
|
251
|
+
var result = this.validate(filePath);
|
|
252
|
+
var wpId = path.basename(filePath, '.md').toUpperCase();
|
|
253
|
+
perFile[wpId] = result;
|
|
254
|
+
|
|
255
|
+
for (var k = 0; k < result.errors.length; k++) {
|
|
256
|
+
allErrors.push(result.errors[k]);
|
|
257
|
+
}
|
|
258
|
+
for (var m = 0; m < result.warnings.length; m++) {
|
|
259
|
+
allWarnings.push(result.warnings[m]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
valid: allErrors.length === 0,
|
|
265
|
+
errors: allErrors,
|
|
266
|
+
warnings: allWarnings,
|
|
267
|
+
results: perFile,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- internal helpers ---
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* 同步读取文件,失败返回 null
|
|
275
|
+
* @param {string} filePath
|
|
276
|
+
* @returns {string|null}
|
|
277
|
+
*/
|
|
278
|
+
_readFile(filePath) {
|
|
279
|
+
try {
|
|
280
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
281
|
+
} catch (err) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 同步读取目录,失败返回空数组
|
|
288
|
+
* @param {string} dirPath
|
|
289
|
+
* @returns {string[]}
|
|
290
|
+
*/
|
|
291
|
+
_readdir(dirPath) {
|
|
292
|
+
try {
|
|
293
|
+
return fs.readdirSync(dirPath);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
module.exports = WorkPackageValidator;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"plugins": [
|
|
4
|
+
{
|
|
5
|
+
"name": "provider-state-store",
|
|
6
|
+
"source": "provider-state-store",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"config": {}
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "provider-role-registry",
|
|
12
|
+
"source": "provider-role-registry",
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"config": {}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "provider-memory-store",
|
|
18
|
+
"source": "provider-memory-store",
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"config": {}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "validator-doc-sync",
|
|
24
|
+
"source": "validator-doc-sync",
|
|
25
|
+
"enabled": true,
|
|
26
|
+
"config": {}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "validator-work-package",
|
|
30
|
+
"source": "validator-work-package",
|
|
31
|
+
"enabled": true,
|
|
32
|
+
"config": {}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "hook-skill-gate",
|
|
36
|
+
"source": "hook-skill-gate",
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"config": {
|
|
39
|
+
"gatedSkills": [],
|
|
40
|
+
"blockedStates": [
|
|
41
|
+
"waiting"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "skill-task-creator",
|
|
47
|
+
"source": "skill-task-creator",
|
|
48
|
+
"enabled": true,
|
|
49
|
+
"config": {}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "skill-batch-task-creator",
|
|
53
|
+
"source": "skill-batch-task-creator",
|
|
54
|
+
"enabled": true,
|
|
55
|
+
"config": {}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "skill-split-work-package",
|
|
59
|
+
"source": "skill-split-work-package",
|
|
60
|
+
"enabled": true,
|
|
61
|
+
"config": {}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "skill-progress-tracker",
|
|
65
|
+
"source": "skill-progress-tracker",
|
|
66
|
+
"enabled": true,
|
|
67
|
+
"config": {}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "skill-team-cleanup",
|
|
71
|
+
"source": "skill-team-cleanup",
|
|
72
|
+
"enabled": true,
|
|
73
|
+
"config": {}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "skill-human-checkpoint",
|
|
77
|
+
"source": "skill-human-checkpoint",
|
|
78
|
+
"enabled": true,
|
|
79
|
+
"config": {}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "skill-role-manager",
|
|
83
|
+
"source": "skill-role-manager",
|
|
84
|
+
"enabled": true,
|
|
85
|
+
"config": {}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "skill-checklist",
|
|
89
|
+
"source": "skill-checklist",
|
|
90
|
+
"enabled": true,
|
|
91
|
+
"config": {}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "skill-completion-report",
|
|
95
|
+
"source": "skill-completion-report",
|
|
96
|
+
"enabled": true,
|
|
97
|
+
"config": {}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "skill-experience-logger",
|
|
101
|
+
"source": "skill-experience-logger",
|
|
102
|
+
"enabled": true,
|
|
103
|
+
"config": {}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "skill-agent-dispatcher",
|
|
107
|
+
"source": "skill-agent-dispatcher",
|
|
108
|
+
"enabled": true,
|
|
109
|
+
"config": {}
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "skill-workflow-orchestrator",
|
|
113
|
+
"source": "skill-workflow-orchestrator",
|
|
114
|
+
"enabled": true,
|
|
115
|
+
"config": {}
|
|
116
|
+
}
|
|
117
|
+
]
|
|
118
|
+
}
|