speckeeper 0.9.2 → 0.9.3
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/README.md +2 -0
- package/cli-contract.yaml +574 -0
- package/docs/cli-reference.md +449 -0
- package/docs/design/actors.md +72 -0
- package/docs/design/arch-actors.md +42 -0
- package/docs/design/artifacts.md +69 -0
- package/docs/design/cli-commands.md +332 -0
- package/docs/design/constraints.md +66 -0
- package/docs/design/containers.md +86 -0
- package/docs/design/entities.md +161 -0
- package/docs/design/external-systems.md +42 -0
- package/docs/design/functional-requirements.md +810 -0
- package/docs/design/glossary.md +242 -0
- package/docs/design/nonfunctional-requirements.md +242 -0
- package/docs/design/test-refs.md +258 -0
- package/docs/design/usecases.md +114 -0
- package/docs/directory-entries.md +22 -0
- package/docs/framework_requirements_spec.md +1052 -0
- package/docs/model-guide.md +762 -0
- package/docs/model_entity_catalog.md +67 -0
- package/docs/scaffold-mermaid-spec.md +357 -0
- package/package.json +7 -2
|
@@ -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
|