@clawplays/ospec-cli 0.3.7 → 0.3.8
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/dist/cli.js +1 -1
- package/dist/commands/ArchiveCommand.js +1 -0
- package/dist/commands/NewCommand.js +19 -11
- package/dist/commands/VerifyCommand.js +4 -40
- package/dist/services/ProjectService.d.ts +1 -0
- package/dist/services/ProjectService.js +257 -406
- package/dist/services/QueueService.js +1 -0
- package/dist/services/TemplateGenerator.js +6 -6
- package/dist/services/templates/ExecutionTemplateBuilder.js +52 -28
- package/dist/services/templates/TemplateBuilderBase.d.ts +5 -0
- package/dist/services/templates/TemplateBuilderBase.js +31 -2
- package/dist/services/templates/TemplateInputFactory.js +4 -0
- package/dist/services/templates/templateTypes.d.ts +4 -0
- package/dist/tools/build-index.js +127 -35
- package/package.json +5 -1
|
@@ -77,6 +77,7 @@ class QueueService {
|
|
|
77
77
|
await this.fileService.writeJSON(statePath, state);
|
|
78
78
|
await this.updateFrontmatterStatus(path_1.default.join(activePath, constants_1.FILE_NAMES.PROPOSAL), 'active');
|
|
79
79
|
await this.updateFrontmatterStatus(path_1.default.join(activePath, constants_1.FILE_NAMES.VERIFICATION), 'verifying');
|
|
80
|
+
await this.projectService.rebaseMovedChangeMarkdownLinks(queuedPath, activePath);
|
|
80
81
|
const item = await this.buildQueuedChangeStatusItem(rootDir, activePath);
|
|
81
82
|
if (!item) {
|
|
82
83
|
throw new Error(`Activated change state could not be read: ${changeName}`);
|
|
@@ -63,20 +63,20 @@ optional_steps: [${optionalSteps.map((s) => `"${s}"`).join(', ')}]
|
|
|
63
63
|
// 添加核心任务
|
|
64
64
|
if (coreRequiredSteps.length > 0) {
|
|
65
65
|
coreRequiredSteps.forEach((step, index) => {
|
|
66
|
-
content += `- [ ] ${
|
|
66
|
+
content += `- [ ] ${step}\n`;
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
else {
|
|
70
|
-
content += `- [ ]
|
|
71
|
-
content += `- [ ]
|
|
72
|
-
content += `- [ ]
|
|
73
|
-
content += `- [ ]
|
|
70
|
+
content += `- [ ] 实现功能\n`;
|
|
71
|
+
content += `- [ ] 更新文档\n`;
|
|
72
|
+
content += `- [ ] 更新索引\n`;
|
|
73
|
+
content += `- [ ] 运行测试\n`;
|
|
74
74
|
}
|
|
75
75
|
// 添加可选任务
|
|
76
76
|
if (optionalSteps.length > 0) {
|
|
77
77
|
content += `\n### 可选任务\n\n`;
|
|
78
78
|
optionalSteps.forEach((step, index) => {
|
|
79
|
-
content += `- [ ] ${
|
|
79
|
+
content += `- [ ] ${step}\n`;
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
return content;
|
|
@@ -9,6 +9,8 @@ class ExecutionTemplateBuilder extends TemplateBuilderBase_1.TemplateBuilderBase
|
|
|
9
9
|
}
|
|
10
10
|
generateProposalTemplate(input) {
|
|
11
11
|
const context = this.inputs.normalizeFeatureTemplateInput(input);
|
|
12
|
+
this.setReferenceDocumentContext(context.projectRoot, context.documentPath);
|
|
13
|
+
try {
|
|
12
14
|
const created = this.getCurrentDate();
|
|
13
15
|
const projectDocs = context.projectContext.projectDocs ?? [];
|
|
14
16
|
const moduleSkills = context.projectContext.moduleSkills ?? [];
|
|
@@ -156,30 +158,36 @@ ${this.formatChecklist(context.acceptanceCriteria, 'قيد التحديد')}`;
|
|
|
156
158
|
affects: context.affects,
|
|
157
159
|
flags: context.flags,
|
|
158
160
|
}, this.copy(context.documentLanguage, zh, en, ja, ar));
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
this.clearReferenceDocumentContext();
|
|
164
|
+
}
|
|
159
165
|
}
|
|
160
166
|
generateTasksTemplate(input) {
|
|
161
167
|
const context = this.inputs.normalizeFeatureTemplateInput(input);
|
|
168
|
+
this.setReferenceDocumentContext(context.projectRoot, context.documentPath);
|
|
169
|
+
try {
|
|
162
170
|
const created = this.getCurrentDate();
|
|
163
171
|
const projectDocs = context.projectContext.projectDocs ?? [];
|
|
164
172
|
const moduleSkills = context.projectContext.moduleSkills ?? [];
|
|
165
173
|
const optionalStepTasksZh = context.optionalSteps.length > 0
|
|
166
174
|
? context.optionalSteps
|
|
167
|
-
.map((step
|
|
175
|
+
.map((step) => `- [ ] 完成可选步骤 \`${step}\` 的文档和验证`)
|
|
168
176
|
.join('\n')
|
|
169
177
|
: '';
|
|
170
178
|
const optionalStepTasksEn = context.optionalSteps.length > 0
|
|
171
179
|
? context.optionalSteps
|
|
172
|
-
.map((step
|
|
180
|
+
.map((step) => `- [ ] Finish docs and verification for optional step \`${step}\``)
|
|
173
181
|
.join('\n')
|
|
174
182
|
: '';
|
|
175
183
|
const optionalStepTasksJa = context.optionalSteps.length > 0
|
|
176
184
|
? context.optionalSteps
|
|
177
|
-
.map((step
|
|
185
|
+
.map((step) => `- [ ] オプション手順 \`${step}\` の文書と検証を完了する`)
|
|
178
186
|
.join('\n')
|
|
179
187
|
: '';
|
|
180
188
|
const optionalStepTasksAr = context.optionalSteps.length > 0
|
|
181
189
|
? context.optionalSteps
|
|
182
|
-
.map((step
|
|
190
|
+
.map((step) => `- [ ] أكمل التوثيق والتحقق للخطوة الاختيارية \`${step}\``)
|
|
183
191
|
.join('\n')
|
|
184
192
|
: '';
|
|
185
193
|
const zh = `## 上下文引用
|
|
@@ -192,12 +200,12 @@ ${this.formatReferenceList(moduleSkills, '待补充')}
|
|
|
192
200
|
|
|
193
201
|
## 任务清单
|
|
194
202
|
|
|
195
|
-
- [ ]
|
|
196
|
-
- [ ]
|
|
197
|
-
- [ ]
|
|
198
|
-
- [ ]
|
|
199
|
-
- [ ]
|
|
200
|
-
- [ ]
|
|
203
|
+
- [ ] 完成实现
|
|
204
|
+
- [ ] 对齐项目规划文档与本次 change 的边界
|
|
205
|
+
- [ ] 更新涉及模块的 \`SKILL.md\`
|
|
206
|
+
- [ ] 更新相关 API / 设计 / 计划文档
|
|
207
|
+
- [ ] 重新生成 \`SKILL.index.json\`
|
|
208
|
+
- [ ] 执行验证并更新 \`verification.md\`
|
|
201
209
|
${optionalStepTasksZh}`.trim();
|
|
202
210
|
const en = `## Context References
|
|
203
211
|
|
|
@@ -209,12 +217,12 @@ ${this.formatReferenceList(moduleSkills, 'TBD')}
|
|
|
209
217
|
|
|
210
218
|
## Task Checklist
|
|
211
219
|
|
|
212
|
-
- [ ]
|
|
213
|
-
- [ ]
|
|
214
|
-
- [ ]
|
|
215
|
-
- [ ]
|
|
216
|
-
- [ ]
|
|
217
|
-
- [ ]
|
|
220
|
+
- [ ] Implement the change
|
|
221
|
+
- [ ] Align project planning docs with this change boundary
|
|
222
|
+
- [ ] Update affected \`SKILL.md\` files
|
|
223
|
+
- [ ] Update related API / design / planning docs
|
|
224
|
+
- [ ] Rebuild \`SKILL.index.json\`
|
|
225
|
+
- [ ] Run verification and update \`verification.md\`
|
|
218
226
|
${optionalStepTasksEn}`.trim();
|
|
219
227
|
const ja = `## 参照コンテキスト
|
|
220
228
|
|
|
@@ -226,12 +234,12 @@ ${this.formatReferenceList(moduleSkills, '未定')}
|
|
|
226
234
|
|
|
227
235
|
## タスクチェックリスト
|
|
228
236
|
|
|
229
|
-
- [ ]
|
|
230
|
-
- [ ]
|
|
231
|
-
- [ ]
|
|
232
|
-
- [ ]
|
|
233
|
-
- [ ]
|
|
234
|
-
- [ ]
|
|
237
|
+
- [ ] change を実装する
|
|
238
|
+
- [ ] この change の境界に合わせてプロジェクト計画文書を揃える
|
|
239
|
+
- [ ] 影響を受ける \`SKILL.md\` を更新する
|
|
240
|
+
- [ ] 関連する API / 設計 / 計画文書を更新する
|
|
241
|
+
- [ ] \`SKILL.index.json\` を再生成する
|
|
242
|
+
- [ ] 検証を実行して \`verification.md\` を更新する
|
|
235
243
|
${optionalStepTasksJa}`.trim();
|
|
236
244
|
const ar = `## مراجع السياق
|
|
237
245
|
|
|
@@ -243,21 +251,27 @@ ${this.formatReferenceList(moduleSkills, 'قيد التحديد')}
|
|
|
243
251
|
|
|
244
252
|
## قائمة المهام
|
|
245
253
|
|
|
246
|
-
- [ ]
|
|
247
|
-
- [ ]
|
|
248
|
-
- [ ]
|
|
249
|
-
- [ ]
|
|
250
|
-
- [ ]
|
|
251
|
-
- [ ]
|
|
254
|
+
- [ ] نفّذ التغيير
|
|
255
|
+
- [ ] وحّد وثائق تخطيط المشروع مع حدود هذا change
|
|
256
|
+
- [ ] حدّث ملفات \`SKILL.md\` المتأثرة
|
|
257
|
+
- [ ] حدّث وثائق API / التصميم / التخطيط ذات الصلة
|
|
258
|
+
- [ ] أعد بناء \`SKILL.index.json\`
|
|
259
|
+
- [ ] نفّذ التحقق وحدّث \`verification.md\`
|
|
252
260
|
${optionalStepTasksAr}`.trim();
|
|
253
261
|
return this.withFrontmatter({
|
|
254
262
|
feature: context.feature,
|
|
255
263
|
created,
|
|
256
264
|
optional_steps: context.optionalSteps,
|
|
257
265
|
}, this.copy(context.documentLanguage, zh, en, ja, ar));
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
this.clearReferenceDocumentContext();
|
|
269
|
+
}
|
|
258
270
|
}
|
|
259
271
|
generateVerificationTemplate(input) {
|
|
260
272
|
const context = this.inputs.normalizeFeatureTemplateInput(input);
|
|
273
|
+
this.setReferenceDocumentContext(context.projectRoot, context.documentPath);
|
|
274
|
+
try {
|
|
261
275
|
const created = this.getCurrentDate();
|
|
262
276
|
const projectDocs = context.projectContext.projectDocs ?? [];
|
|
263
277
|
const moduleSkills = context.projectContext.moduleSkills ?? [];
|
|
@@ -365,9 +379,15 @@ ${this.formatChecklist(context.acceptanceCriteria, 'معيار قبول 1')}
|
|
|
365
379
|
optional_steps: context.optionalSteps,
|
|
366
380
|
passed_optional_steps: [],
|
|
367
381
|
}, this.copy(context.documentLanguage, zh, en, ja, ar));
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
this.clearReferenceDocumentContext();
|
|
385
|
+
}
|
|
368
386
|
}
|
|
369
387
|
generateReviewTemplate(input) {
|
|
370
388
|
const context = this.inputs.normalizeFeatureTemplateInput(input);
|
|
389
|
+
this.setReferenceDocumentContext(context.projectRoot, context.documentPath);
|
|
390
|
+
try {
|
|
371
391
|
const created = this.getCurrentDate();
|
|
372
392
|
const projectDocs = context.projectContext.projectDocs ?? [];
|
|
373
393
|
const moduleSkills = context.projectContext.moduleSkills ?? [];
|
|
@@ -520,6 +540,10 @@ ${this.formatReferenceList(linkedKnowledgeDocs, 'قيد التحديد')}
|
|
|
520
540
|
created,
|
|
521
541
|
status: 'pending_review',
|
|
522
542
|
}, this.copy(context.documentLanguage, zh, en, ja, ar));
|
|
543
|
+
}
|
|
544
|
+
finally {
|
|
545
|
+
this.clearReferenceDocumentContext();
|
|
546
|
+
}
|
|
523
547
|
}
|
|
524
548
|
}
|
|
525
549
|
exports.ExecutionTemplateBuilder = ExecutionTemplateBuilder;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { FeatureProjectReference, TemplateDocumentLanguage } from './templateTypes';
|
|
2
2
|
type FrontmatterValue = string | number | boolean | string[];
|
|
3
3
|
export declare abstract class TemplateBuilderBase {
|
|
4
|
+
private referenceProjectRoot?;
|
|
5
|
+
private referenceDocumentPath?;
|
|
6
|
+
protected setReferenceDocumentContext(projectRoot: string | undefined, documentPath: string | undefined): void;
|
|
7
|
+
protected clearReferenceDocumentContext(): void;
|
|
4
8
|
protected getCurrentDate(): string;
|
|
5
9
|
protected isEnglish(language: TemplateDocumentLanguage): boolean;
|
|
6
10
|
protected copy(language: TemplateDocumentLanguage, zh: string, en: string, ja?: string, ar?: string): string;
|
|
@@ -13,6 +17,7 @@ export declare abstract class TemplateBuilderBase {
|
|
|
13
17
|
protected formatReferenceList(items: FeatureProjectReference[], emptyFallback: string): string;
|
|
14
18
|
protected formatReferenceChecklist(items: FeatureProjectReference[], emptyFallback: string): string;
|
|
15
19
|
protected withFrontmatter(fields: Record<string, FrontmatterValue | undefined>, body: string): string;
|
|
20
|
+
private resolveReferenceHref;
|
|
16
21
|
private toYamlValue;
|
|
17
22
|
}
|
|
18
23
|
export {};
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.TemplateBuilderBase = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
4
8
|
class TemplateBuilderBase {
|
|
9
|
+
setReferenceDocumentContext(projectRoot, documentPath) {
|
|
10
|
+
this.referenceProjectRoot = typeof projectRoot === 'string' && projectRoot.trim().length > 0 ? projectRoot : undefined;
|
|
11
|
+
this.referenceDocumentPath = typeof documentPath === 'string' && documentPath.trim().length > 0 ? documentPath : undefined;
|
|
12
|
+
}
|
|
13
|
+
clearReferenceDocumentContext() {
|
|
14
|
+
this.referenceProjectRoot = undefined;
|
|
15
|
+
this.referenceDocumentPath = undefined;
|
|
16
|
+
}
|
|
5
17
|
getCurrentDate() {
|
|
6
18
|
return new Date().toISOString().slice(0, 10);
|
|
7
19
|
}
|
|
@@ -40,13 +52,17 @@ class TemplateBuilderBase {
|
|
|
40
52
|
if (items.length === 0) {
|
|
41
53
|
return `- ${emptyFallback}`;
|
|
42
54
|
}
|
|
43
|
-
return items
|
|
55
|
+
return items
|
|
56
|
+
.map(item => `- ${item.title}: [${item.path}](${this.resolveReferenceHref(item.path)})`)
|
|
57
|
+
.join('\n');
|
|
44
58
|
}
|
|
45
59
|
formatReferenceChecklist(items, emptyFallback) {
|
|
46
60
|
if (items.length === 0) {
|
|
47
61
|
return `- [ ] ${emptyFallback}`;
|
|
48
62
|
}
|
|
49
|
-
return items
|
|
63
|
+
return items
|
|
64
|
+
.map(item => `- [ ] ${item.title}: [${item.path}](${this.resolveReferenceHref(item.path)})`)
|
|
65
|
+
.join('\n');
|
|
50
66
|
}
|
|
51
67
|
withFrontmatter(fields, body) {
|
|
52
68
|
const frontmatter = Object.entries(fields)
|
|
@@ -55,6 +71,19 @@ class TemplateBuilderBase {
|
|
|
55
71
|
.join('\n');
|
|
56
72
|
return `---\n${frontmatter}\n---\n\n${body.trim()}\n`;
|
|
57
73
|
}
|
|
74
|
+
resolveReferenceHref(referencePath) {
|
|
75
|
+
const normalizedReferencePath = String(referencePath || '').replace(/\\/g, '/');
|
|
76
|
+
if (!normalizedReferencePath) {
|
|
77
|
+
return normalizedReferencePath;
|
|
78
|
+
}
|
|
79
|
+
if (!this.referenceProjectRoot || !this.referenceDocumentPath) {
|
|
80
|
+
return normalizedReferencePath;
|
|
81
|
+
}
|
|
82
|
+
const targetPath = path_1.default.resolve(this.referenceProjectRoot, normalizedReferencePath);
|
|
83
|
+
const documentDir = path_1.default.dirname(this.referenceDocumentPath);
|
|
84
|
+
const relativePath = path_1.default.relative(documentDir, targetPath).replace(/\\/g, '/');
|
|
85
|
+
return relativePath || '.';
|
|
86
|
+
}
|
|
58
87
|
toYamlValue(value) {
|
|
59
88
|
if (Array.isArray(value)) {
|
|
60
89
|
return `[${value.map(item => JSON.stringify(item)).join(', ')}]`;
|
|
@@ -20,6 +20,8 @@ class TemplateInputFactory {
|
|
|
20
20
|
acceptanceCriteria: [...englishDefaults.acceptanceCriteria],
|
|
21
21
|
projectContext: this.normalizeFeatureProjectContext(),
|
|
22
22
|
documentLanguage: 'en-US',
|
|
23
|
+
projectRoot: undefined,
|
|
24
|
+
documentPath: undefined,
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
const documentLanguage = this.normalizeDocumentLanguage(input.documentLanguage);
|
|
@@ -38,6 +40,8 @@ class TemplateInputFactory {
|
|
|
38
40
|
acceptanceCriteria: input.acceptanceCriteria?.map(item => item.trim()).filter(Boolean) ?? [...localizedDefaults.acceptanceCriteria],
|
|
39
41
|
projectContext: this.normalizeFeatureProjectContext(input.projectContext),
|
|
40
42
|
documentLanguage,
|
|
43
|
+
projectRoot: typeof input.projectRoot === 'string' && input.projectRoot.trim().length > 0 ? input.projectRoot.trim() : undefined,
|
|
44
|
+
documentPath: typeof input.documentPath === 'string' && input.documentPath.trim().length > 0 ? input.documentPath.trim() : undefined,
|
|
41
45
|
};
|
|
42
46
|
if (!input.background?.trim()) {
|
|
43
47
|
normalized.background = localizedDefaults.background;
|
|
@@ -15,6 +15,8 @@ export interface FeatureTemplateInput {
|
|
|
15
15
|
acceptanceCriteria?: string[];
|
|
16
16
|
projectContext?: FeatureProjectContext;
|
|
17
17
|
documentLanguage?: TemplateDocumentLanguage;
|
|
18
|
+
projectRoot?: string;
|
|
19
|
+
documentPath?: string;
|
|
18
20
|
}
|
|
19
21
|
export interface NormalizedFeatureTemplateInput {
|
|
20
22
|
feature: string;
|
|
@@ -30,6 +32,8 @@ export interface NormalizedFeatureTemplateInput {
|
|
|
30
32
|
acceptanceCriteria: string[];
|
|
31
33
|
projectContext: FeatureProjectContext;
|
|
32
34
|
documentLanguage: TemplateDocumentLanguage;
|
|
35
|
+
projectRoot?: string;
|
|
36
|
+
documentPath?: string;
|
|
33
37
|
}
|
|
34
38
|
export interface FeatureProjectReference {
|
|
35
39
|
title: string;
|
|
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const fsp = require('fs/promises');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { spawnSync } = require('child_process');
|
|
7
|
+
const matter = require('gray-matter');
|
|
7
8
|
|
|
8
9
|
const SKIP_DIRS = new Set(['node_modules', 'dist', '.git', 'changes', 'for-ai']);
|
|
9
10
|
const INDEX_FILE = 'SKILL.index.json';
|
|
@@ -282,46 +283,31 @@ async function buildChangeSummary(rootDir, changeName, config) {
|
|
|
282
283
|
}
|
|
283
284
|
|
|
284
285
|
if (tasksExists) {
|
|
285
|
-
const tasks =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
missing.length === 0
|
|
294
|
-
? 'All activated optional steps are present in tasks.md'
|
|
295
|
-
: `Missing optional steps in tasks.md: ${missing.join(', ')}`,
|
|
296
|
-
});
|
|
297
|
-
checks.push({
|
|
298
|
-
name: 'tasks.md.checklist',
|
|
299
|
-
status: checklistComplete ? 'pass' : 'warn',
|
|
300
|
-
message: checklistComplete ? 'tasks.md checklist is complete' : 'tasks.md still has unchecked items',
|
|
286
|
+
const tasks = analyzeWorkflowChecklistDocument(await fsp.readFile(tasksPath, 'utf8'), {
|
|
287
|
+
name: 'tasks.md',
|
|
288
|
+
activatedSteps,
|
|
289
|
+
requiredFields: [
|
|
290
|
+
['feature', 'string'],
|
|
291
|
+
['created', 'string_or_date'],
|
|
292
|
+
['optional_steps', 'array'],
|
|
293
|
+
],
|
|
301
294
|
});
|
|
295
|
+
checks.push(...tasks.checks);
|
|
302
296
|
}
|
|
303
297
|
|
|
304
298
|
if (verificationExists) {
|
|
305
|
-
const verification =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
: `Missing optional steps in verification.md: ${missing.join(', ')}`,
|
|
316
|
-
});
|
|
317
|
-
checks.push({
|
|
318
|
-
name: 'verification.md.checklist',
|
|
319
|
-
status: checklistComplete ? 'pass' : 'warn',
|
|
320
|
-
message:
|
|
321
|
-
checklistComplete
|
|
322
|
-
? 'verification.md checklist is complete'
|
|
323
|
-
: 'verification.md still has unchecked items',
|
|
299
|
+
const verification = analyzeWorkflowChecklistDocument(await fsp.readFile(verificationPath, 'utf8'), {
|
|
300
|
+
name: 'verification.md',
|
|
301
|
+
activatedSteps,
|
|
302
|
+
requiredFields: [
|
|
303
|
+
['feature', 'string'],
|
|
304
|
+
['created', 'string_or_date'],
|
|
305
|
+
['status', 'string'],
|
|
306
|
+
['optional_steps', 'array'],
|
|
307
|
+
['passed_optional_steps', 'array'],
|
|
308
|
+
],
|
|
324
309
|
});
|
|
310
|
+
checks.push(...verification.checks);
|
|
325
311
|
}
|
|
326
312
|
|
|
327
313
|
const hasProtocolIssues = checks.some(check => check.status !== 'pass');
|
|
@@ -476,6 +462,93 @@ function parseSkillFile(content) {
|
|
|
476
462
|
};
|
|
477
463
|
}
|
|
478
464
|
|
|
465
|
+
function analyzeWorkflowChecklistDocument(content, options) {
|
|
466
|
+
const hasFrontmatter = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/.test(content);
|
|
467
|
+
let parsed = null;
|
|
468
|
+
let parseError = null;
|
|
469
|
+
|
|
470
|
+
if (hasFrontmatter) {
|
|
471
|
+
try {
|
|
472
|
+
parsed = matter(content);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
parseError = error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const data = parsed?.data ?? {};
|
|
479
|
+
const optionalStepsFieldValid = Array.isArray(data.optional_steps);
|
|
480
|
+
const optionalSteps = optionalStepsFieldValid ? ensureArray(data.optional_steps) : [];
|
|
481
|
+
const invalidRequiredFields = options.requiredFields
|
|
482
|
+
.filter(([fieldName, fieldType]) => !isValidFrontmatterField(data[fieldName], fieldType))
|
|
483
|
+
.map(([fieldName]) => fieldName);
|
|
484
|
+
const missingActivatedSteps = optionalStepsFieldValid
|
|
485
|
+
? options.activatedSteps.filter(step => !optionalSteps.includes(step))
|
|
486
|
+
: [...options.activatedSteps];
|
|
487
|
+
const checklistItems = parsed?.content.match(/^\s*-\s+\[(?: |x|X)\]\s+.+$/gm) ?? [];
|
|
488
|
+
const uncheckedItems = parsed?.content.match(/^\s*-\s+\[ \]\s+.+$/gm) ?? [];
|
|
489
|
+
const checklistStructureValid = checklistItems.length > 0;
|
|
490
|
+
|
|
491
|
+
let frontmatterMessage = `${options.name} frontmatter parsed successfully`;
|
|
492
|
+
if (!hasFrontmatter) {
|
|
493
|
+
frontmatterMessage = `${options.name} is missing a valid frontmatter block`;
|
|
494
|
+
} else if (parseError) {
|
|
495
|
+
frontmatterMessage = `${options.name} frontmatter cannot be parsed: ${parseError.message}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let requiredFieldsMessage = `${options.name} has all required frontmatter fields`;
|
|
499
|
+
if (!hasFrontmatter || parseError) {
|
|
500
|
+
requiredFieldsMessage = `Cannot validate required fields in ${options.name} because frontmatter is invalid`;
|
|
501
|
+
} else if (invalidRequiredFields.length > 0) {
|
|
502
|
+
requiredFieldsMessage = `Missing or invalid required fields in ${options.name}: ${invalidRequiredFields.join(', ')}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let optionalStepsMessage = `All activated optional steps are present in ${options.name}`;
|
|
506
|
+
if (!optionalStepsFieldValid) {
|
|
507
|
+
optionalStepsMessage = `${options.name} frontmatter field optional_steps must be an array`;
|
|
508
|
+
} else if (missingActivatedSteps.length > 0) {
|
|
509
|
+
optionalStepsMessage = `Missing optional steps in ${options.name}: ${missingActivatedSteps.join(', ')}`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
let checklistStatus = 'pass';
|
|
513
|
+
let checklistMessage = `${options.name} checklist is complete`;
|
|
514
|
+
if (!hasFrontmatter || parseError) {
|
|
515
|
+
checklistStatus = 'fail';
|
|
516
|
+
checklistMessage = `${options.name} checklist cannot be validated because frontmatter is invalid`;
|
|
517
|
+
} else if (!checklistStructureValid) {
|
|
518
|
+
checklistStatus = 'fail';
|
|
519
|
+
checklistMessage = `${options.name} must contain at least one Markdown checklist item`;
|
|
520
|
+
} else if (uncheckedItems.length > 0) {
|
|
521
|
+
checklistStatus = 'warn';
|
|
522
|
+
checklistMessage = `${options.name} still has unchecked items`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
optionalSteps,
|
|
527
|
+
checks: [
|
|
528
|
+
{
|
|
529
|
+
name: `${options.name}.frontmatter`,
|
|
530
|
+
status: hasFrontmatter && parseError === null ? 'pass' : 'fail',
|
|
531
|
+
message: frontmatterMessage,
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: `${options.name}.required_fields`,
|
|
535
|
+
status: hasFrontmatter && parseError === null && invalidRequiredFields.length === 0 ? 'pass' : 'fail',
|
|
536
|
+
message: requiredFieldsMessage,
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: `${options.name}.optional_steps`,
|
|
540
|
+
status: optionalStepsFieldValid && missingActivatedSteps.length === 0 ? 'pass' : 'fail',
|
|
541
|
+
message: optionalStepsMessage,
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
name: `${options.name}.checklist`,
|
|
545
|
+
status: checklistStatus,
|
|
546
|
+
message: checklistMessage,
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
479
552
|
function normalizeLineEndings(content) {
|
|
480
553
|
return String(content || '').replace(/\r\n?/g, '\n');
|
|
481
554
|
}
|
|
@@ -517,6 +590,25 @@ function parseFrontmatter(content) {
|
|
|
517
590
|
};
|
|
518
591
|
}
|
|
519
592
|
|
|
593
|
+
function isValidFrontmatterField(value, type) {
|
|
594
|
+
if (type === 'string') {
|
|
595
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (type === 'string_or_date') {
|
|
599
|
+
return (
|
|
600
|
+
(typeof value === 'string' && value.trim().length > 0) ||
|
|
601
|
+
(value instanceof Date && !Number.isNaN(value.getTime()))
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (type === 'array') {
|
|
606
|
+
return Array.isArray(value);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
520
612
|
function parseValue(rawValue) {
|
|
521
613
|
if (rawValue === '') {
|
|
522
614
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clawplays/ospec-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "CLI tool for enforcing ospec workflow",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
|
|
21
21
|
"index:rebuild": "node dist/tools/build-index.js",
|
|
22
22
|
"validate": "node dist/cli/commands/validate.js",
|
|
23
|
+
"release:cut": "node scripts/release-cut.js",
|
|
24
|
+
"release:cut:patch": "node scripts/release-cut.js patch",
|
|
25
|
+
"release:cut:minor": "node scripts/release-cut.js minor",
|
|
26
|
+
"release:cut:major": "node scripts/release-cut.js major",
|
|
23
27
|
"release:smoke": "node scripts/release-smoke.js",
|
|
24
28
|
"release:notes": "node scripts/release-notes.js",
|
|
25
29
|
"release:sync-version": "node scripts/sync-version.js",
|