@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.
@@ -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 += `- [ ] ${index + 1}. ${step}\n`;
66
+ content += `- [ ] ${step}\n`;
67
67
  });
68
68
  }
69
69
  else {
70
- content += `- [ ] 1. 实现功能\n`;
71
- content += `- [ ] 2. 更新文档\n`;
72
- content += `- [ ] 3. 更新索引\n`;
73
- content += `- [ ] 4. 运行测试\n`;
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 += `- [ ] ${index + 1}. ${step}\n`;
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, index) => `- [ ] ${index + 7}. 完成可选步骤 \`${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, index) => `- [ ] ${index + 7}. Finish docs and verification for optional step \`${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, index) => `- [ ] ${index + 7}. オプション手順 \`${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, index) => `- [ ] ${index + 7}. أكمل التوثيق والتحقق للخطوة الاختيارية \`${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
- - [ ] 1. 完成实现
196
- - [ ] 2. 对齐项目规划文档与本次 change 的边界
197
- - [ ] 3. 更新涉及模块的 \`SKILL.md\`
198
- - [ ] 4. 更新相关 API / 设计 / 计划文档
199
- - [ ] 5. 重新生成 \`SKILL.index.json\`
200
- - [ ] 6. 执行验证并更新 \`verification.md\`
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
- - [ ] 1. Implement the change
213
- - [ ] 2. Align project planning docs with this change boundary
214
- - [ ] 3. Update affected \`SKILL.md\` files
215
- - [ ] 4. Update related API / design / planning docs
216
- - [ ] 5. Rebuild \`SKILL.index.json\`
217
- - [ ] 6. Run verification and update \`verification.md\`
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
- - [ ] 1. change を実装する
230
- - [ ] 2. この change の境界に合わせてプロジェクト計画文書を揃える
231
- - [ ] 3. 影響を受ける \`SKILL.md\` を更新する
232
- - [ ] 4. 関連する API / 設計 / 計画文書を更新する
233
- - [ ] 5. \`SKILL.index.json\` を再生成する
234
- - [ ] 6. 検証を実行して \`verification.md\` を更新する
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
- - [ ] 1. نفّذ التغيير
247
- - [ ] 2. وحّد وثائق تخطيط المشروع مع حدود هذا change
248
- - [ ] 3. حدّث ملفات \`SKILL.md\` المتأثرة
249
- - [ ] 4. حدّث وثائق API / التصميم / التخطيط ذات الصلة
250
- - [ ] 5. أعد بناء \`SKILL.index.json\`
251
- - [ ] 6. نفّذ التحقق وحدّث \`verification.md\`
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.map(item => `- ${item.title}: \`${item.path}\``).join('\n');
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.map(item => `- [ ] ${item.path}`).join('\n');
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 = parseFrontmatter(await fsp.readFile(tasksPath, 'utf8'));
286
- const optionalSteps = ensureArray(tasks.data.optional_steps);
287
- const missing = activatedSteps.filter(step => !optionalSteps.includes(step));
288
- const checklistComplete = !/- \[ \]/.test(tasks.body);
289
- checks.push({
290
- name: 'tasks.md.optional_steps',
291
- status: missing.length === 0 ? 'pass' : 'fail',
292
- message:
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 = parseFrontmatter(await fsp.readFile(verificationPath, 'utf8'));
306
- const optionalSteps = ensureArray(verification.data.optional_steps);
307
- const missing = activatedSteps.filter(step => !optionalSteps.includes(step));
308
- const checklistComplete = !/- \[ \]/.test(verification.body);
309
- checks.push({
310
- name: 'verification.md.optional_steps',
311
- status: missing.length === 0 ? 'pass' : 'fail',
312
- message:
313
- missing.length === 0
314
- ? 'All activated optional steps are present in verification.md'
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.7",
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",