speckeeper 0.9.2 → 0.9.4

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.
@@ -0,0 +1,762 @@
1
+ # Model Definition Guide
2
+
3
+ In the speckeeper model system, you define project-specific models by extending the `Model` base class. All models under `design/_models/` (Requirement, Entity, Component, etc.) are defined within the project.
4
+
5
+ ## Overview
6
+
7
+ Defining a model enables the following capabilities:
8
+
9
+ - Define project-specific spec entities (Requirement, Entity, UseCase, RetryPolicy, Runbook, etc.)
10
+ - Type-safe validation with Zod schemas
11
+ - Validation through custom lint rules
12
+ - Automatic documentation generation (Markdown, Mermaid diagrams)
13
+ - Consistency checking with external SSOT (OpenAPI, DDL, etc.)
14
+ - Traceability through model relations
15
+
16
+ ## Architecture
17
+
18
+ The speckeeper model system is designed with the following structure:
19
+
20
+ ```
21
+ ┌─────────────────────────────────────────────────────────────────┐
22
+ │ Model (Model Class): design/_models/ │
23
+ │ = Defines the "type" of specifications │
24
+ │ │
25
+ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
26
+ │ │ RequirementModel│ │ UseCaseModel │ │ EntityModel │ │
27
+ │ │ - schema │ │ - schema │ │ - schema │ │
28
+ │ │ - lintRules │ │ - lintRules │ │ - lintRules │ │
29
+ │ │ - exporters │ │ - exporters │ │ - exporters │ │
30
+ │ │ - modelLevel │ │ - modelLevel │ │ - modelLevel │ │
31
+ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
32
+ └─────────────────────────────────────────────────────────────────┘
33
+
34
+ ┌─────────────────────────────────────────────────────────────────┐
35
+ │ Spec (Spec Instance): design/ │
36
+ │ = Concrete specification data based on models │
37
+ │ │
38
+ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
39
+ │ │ FR-001 │ │ UC-001 │ │ E-001 │ │
40
+ │ │ FR-002 │ │ UC-002 │ │ E-010 │ │
41
+ │ │ ... │ │ ... │ │ ... │ │
42
+ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
43
+ └─────────────────────────────────────────────────────────────────┘
44
+ ```
45
+
46
+ ## Model Definition Examples
47
+
48
+ Below are examples of models actually defined in the speckeeper project.
49
+
50
+ ### APIRef Model (with External SSOT Checker)
51
+
52
+ <!--@embedoc:code_snippet file="design/_models/api-ref.ts" lang="typescript" title="design/_models/api-ref.ts" no_source="true"-->
53
+ ⚠️ File not found: design/_models/api-ref.ts
54
+ <!--@embedoc:end-->
55
+
56
+ ### Model Registration (design/_models/index.ts)
57
+
58
+ After adding a model, register it in the `allModels` array in `design/_models/index.ts`:
59
+
60
+ <!--@embedoc:code_snippet file="design/_models/index.ts" start="77" end="127" lang="typescript" title="design/_models/index.ts (excerpt)" no_source="true"-->
61
+ **design/_models/index.ts (excerpt)**
62
+
63
+ ```typescript
64
+ ArtifactModel.instance,
65
+ DirectoryEntryModel.instance,
66
+ CLICommandModel.instance,
67
+ TestRefModel.instance,
68
+ ];
69
+
70
+ ```
71
+ <!--@embedoc:end-->
72
+
73
+ ---
74
+
75
+ ## Model Base Class
76
+
77
+ All models extend the `Model` base class from `src/core/model.ts`.
78
+
79
+ ### Type Definitions
80
+
81
+ <!--@embedoc:code_snippet file="src/core/model.ts" start="24" end="107" lang="typescript" title="src/core/model.ts (Type Definitions)" no_source="true"-->
82
+ **src/core/model.ts (Type Definitions)**
83
+
84
+ ```typescript
85
+ /**
86
+ * Lint rule definition
87
+ */
88
+ export interface LintRule<T> {
89
+ id: string;
90
+ severity: 'error' | 'warning' | 'info';
91
+ message: string;
92
+ check: (spec: T) => boolean; // true if there is an issue
93
+ }
94
+
95
+ /**
96
+ * Lint result
97
+ */
98
+ export interface LintResult {
99
+ ruleId: string;
100
+ severity: 'error' | 'warning' | 'info';
101
+ message: string;
102
+ specId?: string;
103
+ }
104
+
105
+ /**
106
+ * Exporter definition
107
+ */
108
+ export interface Exporter<T> {
109
+ format: 'markdown' | 'json' | 'mermaid';
110
+ single?: (spec: T) => string;
111
+ index?: (specs: T[]) => string;
112
+ outputDir?: string;
113
+ filename?: (spec: T) => string;
114
+ }
115
+
116
+ /**
117
+ * External checker definition
118
+ */
119
+ export interface ExternalChecker<T> {
120
+ targetType: string; // 'openapi' | 'ddl' | 'cli' etc.
121
+ sourcePath: (spec: T) => string;
122
+ check: (spec: T, externalData: unknown) => CheckResult;
123
+ }
124
+
125
+ /**
126
+ * Check result
127
+ */
128
+ export interface CheckResult {
129
+ success: boolean;
130
+ errors: { message: string; field?: string; specId?: string }[];
131
+ warnings: { message: string; field?: string; specId?: string }[];
132
+ /** Files where annotations matching this spec were found */
133
+ matchedFiles?: Array<{
134
+ specId: string;
135
+ filePath: string;
136
+ line: number;
137
+ relationType: 'verifiedBy' | 'implements' | 'traces';
138
+ }>;
139
+ }
140
+
141
+ /**
142
+ * Coverage result
143
+ */
144
+ export interface CoverageResult {
145
+ /** Total target count */
146
+ total: number;
147
+ /** Covered count */
148
+ covered: number;
149
+ /** Uncovered count */
150
+ uncovered: number;
151
+ /** Coverage rate (%) */
152
+ coveragePercent: number;
153
+ /** Details of covered items */
154
+ coveredItems: { id: string; description?: string }[];
155
+ /** Details of uncovered items */
156
+ uncoveredItems: { id: string; description?: string; sourceId?: string }[];
157
+ }
158
+
159
+ /**
160
+ * Coverage checker definition
161
+ *
162
+ * Verify cross-model consistency (coverage).
163
+ * Example: Whether TestRef covers acceptanceCriteria of Requirement
164
+ */
165
+ export interface CoverageChecker<T> {
166
+ /** Target model ID for coverage (e.g. 'requirement') */
167
+ targetModel: string;
168
+ /** Description of coverage check */
169
+ ```
170
+ <!--@embedoc:end-->
171
+
172
+ ### Model Class
173
+
174
+ <!--@embedoc:code_snippet file="src/core/model.ts" start="109" end="156" lang="typescript" title="src/core/model.ts (Model Class Properties)" no_source="true"-->
175
+ **src/core/model.ts (Model Class Properties)**
176
+
177
+ ```typescript
178
+ /** Execute coverage check */
179
+ check: (
180
+ specs: T[],
181
+ registry: Record<string, Map<string, unknown>>
182
+ ) => CoverageResult;
183
+ }
184
+
185
+ // ============================================================================
186
+ // Renderer (for embeds)
187
+ // ============================================================================
188
+
189
+ /**
190
+ * Render context
191
+ * Simplified version compatible with embedoc's EmbedContext
192
+ */
193
+ export interface RenderContext {
194
+ /** Parameters (filter conditions other than format, etc.) */
195
+ params: Record<string, string | undefined>;
196
+ /** Markdown helpers */
197
+ markdown: {
198
+ /** Generate table */
199
+ table: (headers: string[], rows: (string | unknown)[][]) => string;
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Renderer definition
205
+ *
206
+ * Model-specific rendering called from embeds
207
+ */
208
+ export interface Renderer<T> {
209
+ /** Format ID ('table', 'list', 'detail', 'spec-chapter', etc.) */
210
+ format: string;
211
+ /** Rendering process */
212
+ render: (specs: T[], ctx: RenderContext) => string;
213
+ }
214
+
215
+ // ============================================================================
216
+ // Model Base Class
217
+ // ============================================================================
218
+
219
+ /**
220
+ * Model base class
221
+ *
222
+ * @template TSchema - Zod schema type
223
+ */
224
+ export abstract class Model<TSchema extends ZodType> {
225
+ /** Singleton instance storage (per subclass) */
226
+ ```
227
+ <!--@embedoc:end-->
228
+
229
+ ### Model Level (ModelLevel)
230
+
231
+ Defines the abstraction level of a model:
232
+
233
+ | Level | Name | Description | Examples |
234
+ |-------|------|-------------|----------|
235
+ | `L0` | Business + Domain | Why / Problem space | UseCase, Actor, Term |
236
+ | `L1` | Requirements | What | Requirement, Constraint |
237
+ | `L2` | Design | How / Strategy | Component, Entity, Layer |
238
+ | `L3` | Detailed Design / Implementation | How to build | Screen, APIRef, TableRef |
239
+
240
+ ### Relation Types
241
+
242
+ | Type | Direction Constraint | Description |
243
+ |------|----------------------|-------------|
244
+ | `implements` | spec→external | Spec is implemented as external artifact (OpenAPI, DDL) |
245
+ | `satisfies` | L1→L0 | Satisfies a use case |
246
+ | `refines` | Same level or lower | Refinement |
247
+ | `verifiedBy` | spec→test | Spec is verified by external test code |
248
+ | `verifies` | test→implementation | Test verifies implementation code (external, no checker generated) |
249
+ | `dependsOn` | None | Dependency |
250
+ | `relatedTo` | None | Association |
251
+
252
+ ---
253
+
254
+ ## Other Model Examples
255
+
256
+ ### TestRef Model (with Coverage Checker)
257
+
258
+ An example implementing both an external SSOT checker and a coverage checker:
259
+
260
+ <!--@embedoc:code_snippet file="design/_models/test-ref.ts" lang="typescript" title="design/_models/test-ref.ts" no_source="true"-->
261
+ **design/_models/test-ref.ts**
262
+
263
+ ```typescript
264
+ /**
265
+ * Test Reference Model Definition
266
+ *
267
+ * Manages the association between test code and requirements/CLI commands.
268
+ * Checks test code existence and requirement ID mentions as external SSOT verification.
269
+ */
270
+ import { z } from 'zod';
271
+ import { Model, RelationSchema } from '../../src/core/model.ts';
272
+ import type { LintRule, Exporter, ExternalChecker, CheckResult, CoverageChecker, CoverageResult, ModelLevel } from '../../src/core/model.ts';
273
+ import { arrayMinLength, idFormat } from '../../src/core/dsl/index.ts';
274
+ import { existsSync, readFileSync } from 'node:fs';
275
+ import { join } from 'node:path';
276
+ import { glob } from 'glob';
277
+
278
+ // ============================================================================
279
+ // Schema Definition
280
+ // ============================================================================
281
+
282
+ /**
283
+ * Test case pattern - Association between acceptance criteria ID and test case
284
+ */
285
+ export const TestCasePatternSchema = z.object({
286
+ /** Related acceptance criteria ID (e.g., FR-101-01) */
287
+ acceptanceCriteriaId: z.string(),
288
+ /** Test case name pattern (regex) */
289
+ pattern: z.string(),
290
+ /** Description (optional, can be derived from acceptance criteria) */
291
+ description: z.string().optional(),
292
+ });
293
+
294
+ /**
295
+ * Test source information
296
+ */
297
+ export const TestSourceSchema = z.object({
298
+ /** Test file path (glob pattern allowed) */
299
+ path: z.string(),
300
+ /** Test framework */
301
+ framework: z.enum(['vitest', 'jest', 'mocha', 'playwright', 'cypress']),
302
+ /** Test result JSON path (optional) */
303
+ resultPath: z.string().optional(),
304
+ });
305
+
306
+ /**
307
+ * TestRef Schema
308
+ */
309
+ export const TestRefSchema = z.object({
310
+ /** Unique ID */
311
+ id: z.string(),
312
+ /** Test suite description */
313
+ description: z.string(),
314
+ /** Test source */
315
+ source: TestSourceSchema,
316
+ /** Array of requirement IDs this test verifies */
317
+ verifiesRequirements: z.array(z.string()).min(1),
318
+ /** CLI command ID this test implements (optional) */
319
+ implementsCommand: z.string().optional(),
320
+ /** Association between requirement IDs and test case patterns */
321
+ testCasePatterns: z.array(TestCasePatternSchema).optional(),
322
+ /** Inter-model relation */
323
+ relations: z.array(RelationSchema).optional(),
324
+ });
325
+
326
+ // ============================================================================
327
+ // Type Export
328
+ // ============================================================================
329
+
330
+ export type TestCasePattern = z.infer<typeof TestCasePatternSchema>;
331
+ export type TestSource = z.infer<typeof TestSourceSchema>;
332
+ export type TestRef = z.input<typeof TestRefSchema>;
333
+
334
+ // ============================================================================
335
+ // Helper Functions
336
+ // ============================================================================
337
+
338
+ /**
339
+ * Check requirement ID mentions in test file content
340
+ */
341
+ function checkRequirementMentions(
342
+ filePath: string,
343
+ requirementIds: string[],
344
+ ): { found: string[]; missing: string[] } {
345
+ const found: string[] = [];
346
+ const missing: string[] = [];
347
+
348
+ try {
349
+ const content = readFileSync(filePath, 'utf-8');
350
+ for (const reqId of requirementIds) {
351
+ // Check if requirement ID is mentioned in describe, it, or test
352
+ const patterns = [
353
+ new RegExp(`describe\\s*\\(\\s*['"\`].*${reqId}`, 'm'),
354
+ new RegExp(`it\\s*\\(\\s*['"\`].*${reqId}`, 'm'),
355
+ new RegExp(`test\\s*\\(\\s*['"\`].*${reqId}`, 'm'),
356
+ ];
357
+ const mentioned = patterns.some((p) => p.test(content));
358
+ if (mentioned) {
359
+ found.push(reqId);
360
+ } else {
361
+ missing.push(reqId);
362
+ }
363
+ }
364
+ } catch {
365
+ // Treat all file read errors as missing
366
+ missing.push(...requirementIds);
367
+ }
368
+
369
+ return { found, missing };
370
+ }
371
+
372
+ /**
373
+ * Check test case pattern matches
374
+ */
375
+ function checkTestCasePatterns(
376
+ filePath: string,
377
+ patterns: TestCasePattern[],
378
+ ): { matched: TestCasePattern[]; unmatched: TestCasePattern[] } {
379
+ const matched: TestCasePattern[] = [];
380
+ const unmatched: TestCasePattern[] = [];
381
+
382
+ try {
383
+ const content = readFileSync(filePath, 'utf-8');
384
+ for (const p of patterns) {
385
+ const regex = new RegExp(p.pattern, 'm');
386
+ if (regex.test(content)) {
387
+ matched.push(p);
388
+ } else {
389
+ unmatched.push(p);
390
+ }
391
+ }
392
+ } catch {
393
+ unmatched.push(...patterns);
394
+ }
395
+
396
+ return { matched, unmatched };
397
+ }
398
+
399
+ /**
400
+ * Load and validate test result JSON
401
+ */
402
+ interface VitestResult {
403
+ success: boolean;
404
+ testResults: Array<{
405
+ name: string;
406
+ status: 'passed' | 'failed' | 'skipped';
407
+ assertionResults: Array<{
408
+ fullName: string;
409
+ status: 'passed' | 'failed' | 'skipped';
410
+ }>;
411
+ }>;
412
+ }
413
+
414
+ function checkTestResults(
415
+ resultPath: string,
416
+ requirementIds: string[],
417
+ ): { passed: string[]; failed: string[]; notFound: string[] } {
418
+ const passed: string[] = [];
419
+ const failed: string[] = [];
420
+ const notFound: string[] = [];
421
+
422
+ try {
423
+ const content = readFileSync(resultPath, 'utf-8');
424
+ const results: VitestResult = JSON.parse(content);
425
+
426
+ for (const reqId of requirementIds) {
427
+ let foundTest = false;
428
+ let allPassed = true;
429
+
430
+ for (const testResult of results.testResults) {
431
+ // Check if requirement ID is in test name or assertion name
432
+ if (testResult.name.includes(reqId)) {
433
+ foundTest = true;
434
+ if (testResult.status !== 'passed') {
435
+ allPassed = false;
436
+ }
437
+ }
438
+ for (const assertion of testResult.assertionResults) {
439
+ if (assertion.fullName.includes(reqId)) {
440
+ foundTest = true;
441
+ if (assertion.status !== 'passed') {
442
+ allPassed = false;
443
+ }
444
+ }
445
+ }
446
+ }
447
+
448
+ if (!foundTest) {
449
+ notFound.push(reqId);
450
+ } else if (allPassed) {
451
+ passed.push(reqId);
452
+ } else {
453
+ failed.push(reqId);
454
+ }
455
+ }
456
+ } catch {
457
+ notFound.push(...requirementIds);
458
+ }
459
+
460
+ return { passed, failed, notFound };
461
+ }
462
+
463
+ // ============================================================================
464
+ // Model Class
465
+ // ============================================================================
466
+
467
+ class TestRefModel extends Model<typeof TestRefSchema> {
468
+ readonly id = 'test-ref';
469
+ readonly name = 'TestRef';
470
+ readonly idPrefix = 'TEST';
471
+ readonly schema = TestRefSchema;
472
+ readonly description = 'Test reference (association between test code and requirements)';
473
+ readonly externalSsotType = 'Test Code';
474
+ protected modelLevel: ModelLevel = 'L3';
475
+
476
+ protected lintRules: LintRule<TestRef>[] = [
477
+ arrayMinLength<TestRef>('verifiesRequirements', 1),
478
+ {
479
+ id: 'test-has-source',
480
+ severity: 'error',
481
+ message: 'TestRef must have a test source path',
482
+ check: (spec) => !spec.source?.path,
483
+ },
484
+ idFormat<TestRef>('TEST'),
485
+ {
486
+ id: 'test-has-patterns',
487
+ severity: 'info',
488
+ message: 'TestRef should have test case patterns for specific requirement verification',
489
+ check: (spec) => !spec.testCasePatterns || spec.testCasePatterns.length === 0,
490
+ },
491
+ ];
492
+
493
+ protected exporters: Exporter<TestRef>[] = [
494
+ {
495
+ format: 'markdown',
496
+ single: (spec) => {
497
+ const lines: string[] = [];
498
+ lines.push(`# ${spec.id}: ${spec.description}`);
499
+ lines.push('');
500
+ lines.push('## Test Source');
501
+ lines.push('');
502
+ lines.push(`- **Path**: \`${spec.source.path}\``);
503
+ lines.push(`- **Framework**: ${spec.source.framework}`);
504
+ if (spec.source.resultPath) {
505
+ lines.push(`- **Result JSON**: \`${spec.source.resultPath}\``);
506
+ }
507
+ lines.push('');
508
+
509
+ lines.push('## Verified Requirements');
510
+ lines.push('');
511
+ for (const reqId of spec.verifiesRequirements) {
512
+ lines.push(`- ${reqId}`);
513
+ }
514
+ lines.push('');
515
+
516
+ if (spec.implementsCommand) {
517
+ lines.push('## Implemented Command');
518
+ lines.push('');
519
+ lines.push(`- ${spec.implementsCommand}`);
520
+ lines.push('');
521
+ }
522
+
523
+ if (spec.testCasePatterns && spec.testCasePatterns.length > 0) {
524
+ lines.push('## Test Case Patterns');
525
+ lines.push('');
526
+ lines.push('| Acceptance Criteria ID | Pattern | Description |');
527
+ lines.push('|------------------------|---------|-------------|');
528
+ for (const p of spec.testCasePatterns) {
529
+ lines.push(`| ${p.acceptanceCriteriaId} | \`${p.pattern}\` | ${p.description || '-'} |`);
530
+ }
531
+ lines.push('');
532
+ }
533
+
534
+ return lines.join('\n');
535
+ },
536
+ index: (specs) => {
537
+ const lines: string[] = [];
538
+ lines.push('# Test Reference List');
539
+ lines.push('');
540
+ lines.push('| ID | Description | Framework | Requirements Count |');
541
+ lines.push('|----|-------------|-----------|-------------------|');
542
+ for (const spec of specs) {
543
+ lines.push(
544
+ `| [${spec.id}](./${spec.id}.md) | ${spec.description} | ${spec.source.framework} | ${spec.verifiesRequirements.length} |`,
545
+ );
546
+ }
547
+ return lines.join('\n');
548
+ },
549
+ outputDir: 'test-refs',
550
+ filename: (spec) => spec.id,
551
+ },
552
+ ];
553
+
554
+ protected externalChecker: ExternalChecker<TestRef> = {
555
+ targetType: 'test',
556
+ sourcePath: (spec) => spec.source.path,
557
+ check: (spec): CheckResult => {
558
+ const errors: CheckResult['errors'] = [];
559
+ const warnings: CheckResult['warnings'] = [];
560
+ const basePath = process.cwd();
561
+
562
+ // 1. Check test file existence
563
+ const pattern = spec.source.path;
564
+ const testFiles = glob.sync(pattern, { cwd: basePath });
565
+
566
+ if (testFiles.length === 0) {
567
+ errors.push({
568
+ message: `Test file(s) not found: ${pattern}`,
569
+ specId: spec.id,
570
+ field: 'source.path',
571
+ });
572
+ return { success: false, errors, warnings };
573
+ }
574
+
575
+ // 2. Check requirement ID mentions in each test file
576
+ const allMissing = new Set<string>(spec.verifiesRequirements);
577
+
578
+ for (const testFile of testFiles) {
579
+ const fullPath = join(basePath, testFile);
580
+ const { found } = checkRequirementMentions(fullPath, spec.verifiesRequirements);
581
+ for (const id of found) {
582
+ allMissing.delete(id);
583
+ }
584
+ }
585
+
586
+ if (allMissing.size > 0) {
587
+ for (const reqId of allMissing) {
588
+ warnings.push({
589
+ message: `Requirement '${reqId}' not mentioned in test file(s)`,
590
+ specId: spec.id,
591
+ field: 'verifiesRequirements',
592
+ });
593
+ }
594
+ }
595
+
596
+ // 3. Check test case pattern matches
597
+ if (spec.testCasePatterns && spec.testCasePatterns.length > 0) {
598
+ const allUnmatched = new Map<string, TestCasePattern>();
599
+ for (const p of spec.testCasePatterns) {
600
+ allUnmatched.set(p.acceptanceCriteriaId, p);
601
+ }
602
+
603
+ for (const testFile of testFiles) {
604
+ const fullPath = join(basePath, testFile);
605
+ const { matched } = checkTestCasePatterns(fullPath, spec.testCasePatterns);
606
+ for (const p of matched) {
607
+ allUnmatched.delete(p.acceptanceCriteriaId);
608
+ }
609
+ }
610
+
611
+ for (const [, pattern] of allUnmatched) {
612
+ errors.push({
613
+ message: `Test case pattern not matched for '${pattern.acceptanceCriteriaId}': ${pattern.pattern}`,
614
+ specId: spec.id,
615
+ field: 'testCasePatterns',
616
+ });
617
+ }
618
+ }
619
+
620
+ // 4. Check test results (when resultPath is specified)
621
+ if (spec.source.resultPath) {
622
+ const resultFullPath = join(basePath, spec.source.resultPath);
623
+ if (existsSync(resultFullPath)) {
624
+ const { failed, notFound } = checkTestResults(resultFullPath, spec.verifiesRequirements);
625
+
626
+ for (const reqId of failed) {
627
+ errors.push({
628
+ message: `Test for requirement '${reqId}' failed`,
629
+ specId: spec.id,
630
+ field: 'verifiesRequirements',
631
+ });
632
+ }
633
+
634
+ for (const reqId of notFound) {
635
+ warnings.push({
636
+ message: `No test result found for requirement '${reqId}'`,
637
+ specId: spec.id,
638
+ field: 'verifiesRequirements',
639
+ });
640
+ }
641
+ } else {
642
+ warnings.push({
643
+ message: `Test result file not found: ${spec.source.resultPath}`,
644
+ specId: spec.id,
645
+ field: 'source.resultPath',
646
+ });
647
+ }
648
+ }
649
+
650
+ return {
651
+ success: errors.length === 0,
652
+ errors,
653
+ warnings,
654
+ };
655
+ },
656
+ };
657
+
658
+ /**
659
+ * Coverage Checker
660
+ *
661
+ * Verifies that Requirement acceptanceCriteria (verificationMethod: 'test')
662
+ * are covered by TestRef.testCasePatterns.
663
+ *
664
+ * Note: verificationMethod is a property defined in design/_models/requirement.ts
665
+ */
666
+ protected coverageChecker: CoverageChecker<TestRef> = {
667
+ targetModel: 'requirement',
668
+ description: 'TestRef coverage verification for acceptanceCriteria (verificationMethod: test)',
669
+ check: (specs, registry): CoverageResult => {
670
+ // 1. Extract acceptanceCriteria with verificationMethod: 'test' from requirement model
671
+ const requirements = registry.requirements;
672
+ if (!requirements) {
673
+ return {
674
+ total: 0,
675
+ covered: 0,
676
+ uncovered: 0,
677
+ coveragePercent: 100,
678
+ coveredItems: [],
679
+ uncoveredItems: [],
680
+ };
681
+ }
682
+
683
+ interface AcceptanceCriteriaSpec {
684
+ id: string;
685
+ description: string;
686
+ verificationMethod?: string;
687
+ }
688
+
689
+ interface RequirementSpec {
690
+ id: string;
691
+ acceptanceCriteria?: AcceptanceCriteriaSpec[];
692
+ }
693
+
694
+ const testableACs: Array<{ id: string; description: string; sourceId: string }> = [];
695
+ for (const req of requirements.values() as IterableIterator<RequirementSpec>) {
696
+ if (!req.acceptanceCriteria) continue;
697
+ for (const ac of req.acceptanceCriteria) {
698
+ // design/ specific: only target verificationMethod: 'test'
699
+ if (ac.verificationMethod === 'test') {
700
+ testableACs.push({
701
+ id: ac.id,
702
+ description: ac.description,
703
+ sourceId: req.id,
704
+ });
705
+ }
706
+ }
707
+ }
708
+
709
+ // 2. Collect acceptanceCriteriaId from TestRef.testCasePatterns
710
+ const coveredACIds = new Set<string>();
711
+ for (const ref of specs) {
712
+ if (!ref.testCasePatterns) continue;
713
+ for (const pattern of ref.testCasePatterns) {
714
+ coveredACIds.add(pattern.acceptanceCriteriaId);
715
+ }
716
+ }
717
+
718
+ // 3. Determine coverage
719
+ const coveredItems: CoverageResult['coveredItems'] = [];
720
+ const uncoveredItems: CoverageResult['uncoveredItems'] = [];
721
+
722
+ for (const ac of testableACs) {
723
+ if (coveredACIds.has(ac.id)) {
724
+ coveredItems.push({ id: ac.id, description: ac.description });
725
+ } else {
726
+ uncoveredItems.push({ id: ac.id, description: ac.description, sourceId: ac.sourceId });
727
+ }
728
+ }
729
+
730
+ const total = testableACs.length;
731
+ const covered = coveredItems.length;
732
+ const uncovered = uncoveredItems.length;
733
+ const coveragePercent = total > 0 ? Math.round((covered / total) * 100) : 100;
734
+
735
+ return {
736
+ total,
737
+ covered,
738
+ uncovered,
739
+ coveragePercent,
740
+ coveredItems,
741
+ uncoveredItems,
742
+ };
743
+ },
744
+ };
745
+ }
746
+
747
+ // Singleton instance
748
+ export { TestRefModel };
749
+
750
+ ```
751
+ <!--@embedoc:end-->
752
+
753
+ ---
754
+
755
+ ## Notes
756
+
757
+ 1. **ID Uniqueness**: Ensure the model's `id` does not duplicate other models
758
+ 2. **Schema Validation**: Strict Zod schema definitions are recommended
759
+ 3. **Model Level**: Set `modelLevel` appropriately to enable relation constraint validation
760
+ 4. **Lint Rule Severity**: `error` requires a fix, `warning` is recommended, `info` is informational
761
+ 5. **File Placement**: Model classes go in `design/_models/`, spec instances go in `design/`
762
+ 6. **Model Registration**: Add to the `allModels` array in `design/_models/index.ts` to make it available in CLI and embedoc