phasegate 0.150.0 → 0.150.1
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/CHANGELOG.md +7 -0
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/docs/guide/cli-reference.md +13 -0
- package/package.json +1 -1
- package/scripts/harness/main.ts +16 -0
- package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +71 -0
- package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +161 -0
- package/scripts/harness/nyquist-validation/composition-root.ts +24 -1
- package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +42 -0
- package/scripts/harness/nyquist-validation/index.ts +9 -1
- package/scripts/harness/nyquist-validation/infrastructure/adapters/file-system-generated-matrix-adapter.ts +24 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +37 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +54 -0
- package/scripts/harness/nyquist-validation/presentation/handlers/generate-matrix-handler.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.150.1] - 2026-05-12
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-125 post-publish dogfood** — republish patch for `phasegate:generate-matrix` after `phasegate@0.150.0` dogfood showed the published tarball did not include the new command.
|
|
15
|
+
|
|
10
16
|
## [0.150.0] - 2026-05-12
|
|
11
17
|
|
|
12
18
|
### Added
|
|
13
19
|
|
|
14
20
|
- **G1 / WI-123 / WI-124 / WI-127 / WI-128 — Gate reliability and operational transparency** — `phasegate:status --json` now reports hook/baseline operational health, CI template validator IDs are derived from the live validator-system registry, L4 scheduled audit docs/templates use canonical L4 commands, and README/guide feature inventory matches the shipped 30 skills.
|
|
21
|
+
- **G2 / WI-125 / WI-129 / WI-130 / WI-131 — L2 test-quality core and Nyquist matrix generation** — `L2-003 test-quality` uses semantic AAA / assertion strength analysis, and `phasegate:generate-matrix` generates `requirement-test-matrix.json` from product ACs and test metadata with missing/orphan/intent coverage reporting.
|
|
15
22
|
|
|
16
23
|
## [0.147.0] - 2026-05-12
|
|
17
24
|
|
package/README.ja.md
CHANGED
|
@@ -498,7 +498,7 @@ reports/
|
|
|
498
498
|
|---|---|
|
|
499
499
|
| **[WI-128](docs/inception/_cross/WI-128/description.md)** | L4 運用ロールアウトの仕上げ。`doc-freshness` / `pointer-validation` は L4-004 / L4-005 として登録済みで、`p2:*` 互換コマンドも維持。WI-033 は完了済みとして閉じ、残りの scheduling / default / 運用 docs は後続 WI で扱う。@work-item-id WI-128 |
|
|
500
500
|
|
|
501
|
-
L3 Nyquist Validation の `requirement-test-matrix.json`
|
|
501
|
+
L3 Nyquist Validation の `requirement-test-matrix.json` は `phasegate:generate-matrix` で product docs の受け入れ基準と test metadata から生成できます。`--json` では missing tests / orphan tests / preserved references / intent coverage warnings を確認できます。
|
|
502
502
|
|
|
503
503
|
---
|
|
504
504
|
|
package/README.md
CHANGED
|
@@ -559,7 +559,7 @@ The main path is ready for project use, but a few documented behaviors still req
|
|
|
559
559
|
|---|---|---|
|
|
560
560
|
| **[WI-128](docs/inception/_cross/WI-128/description.md)** | Finish L4 operational rollout | `doc-freshness` and `pointer-validation` are registered as L4-004/L4-005 and also remain available through `p2:*` compatibility commands. WI-033 is closed; remaining rollout polish around scheduling, defaults, and operational docs is tracked separately. @work-item-id WI-128 |
|
|
561
561
|
|
|
562
|
-
`requirement-test-matrix.json`
|
|
562
|
+
`requirement-test-matrix.json` can be generated with `phasegate:generate-matrix` from product acceptance criteria and test metadata, then consumed by L3 Nyquist Validation. Use `--json` to inspect missing tests, orphan tests, preserved references, and intent coverage warnings.
|
|
563
563
|
|
|
564
564
|
---
|
|
565
565
|
|
|
@@ -362,3 +362,16 @@ ISSUE-005 P3-10 で明確化された境界:
|
|
|
362
362
|
| `p2:check-freshness` | `--pattern <glob>` `--dry-run` `--format text\|json` | Compatibility entry point for L4-004 doc freshness; canonical L4 execution is `validate --layer L4` |
|
|
363
363
|
| `p2:validate-pointers` | `--include-urls` `--format text\|json` | Compatibility entry point for L4-005 pointer validation; canonical L4 execution is `validate --layer L4` |
|
|
364
364
|
| `p2:generate-e2e-template` | `--phase <phase>` `--output <path>` | Generate E2E test template |
|
|
365
|
+
|
|
366
|
+
### `phasegate:generate-matrix`
|
|
367
|
+
|
|
368
|
+
<!-- @work-item-id WI-125, WI-131 -->
|
|
369
|
+
|
|
370
|
+
Generates `.harness/requirement-test-matrix.json` from product acceptance criteria and test metadata.
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
phasegate phasegate:generate-matrix --requirements docs/product/user_stories.md --tests scripts/harness/__tests__ --out .harness/requirement-test-matrix.json
|
|
374
|
+
phasegate validate --layer L3
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Use `--json` to inspect `missingTests`, `orphanTests`, preserved references, and intent coverage.
|
package/package.json
CHANGED
package/scripts/harness/main.ts
CHANGED
|
@@ -185,6 +185,7 @@ Commands:
|
|
|
185
185
|
phasegate:lint Lint via harness-api (--target <path>, --json)
|
|
186
186
|
phasegate:complete-check Complete L2-L4 check (--json)
|
|
187
187
|
phasegate:impact-analysis Impact analysis for story (<storyId>, --json)
|
|
188
|
+
phasegate:generate-matrix Generate requirement-test matrix (--requirements, --tests, --out, --json)
|
|
188
189
|
|
|
189
190
|
ci:generate-template Generate CI template (--preset <id>, default: standard; --type <aidlc-gate|consistency-check|pre-commit|agent-context-refresh>, --render, --json)
|
|
190
191
|
ci:migrate-agents-md Migrate AGENTS.md (--dry-run, --validate-only, --json)
|
|
@@ -1652,6 +1653,21 @@ async function main(): Promise<void> {
|
|
|
1652
1653
|
break;
|
|
1653
1654
|
}
|
|
1654
1655
|
|
|
1656
|
+
case "phasegate:generate-matrix": {
|
|
1657
|
+
const { createNyquistValidationModule } = await import("./nyquist-validation/composition-root.js");
|
|
1658
|
+
const mod = createNyquistValidationModule({
|
|
1659
|
+
getStoryIds: async () => [],
|
|
1660
|
+
});
|
|
1661
|
+
const flags: Record<string, boolean | string> = {};
|
|
1662
|
+
if (json) flags.json = true;
|
|
1663
|
+
await mod.handlers.generateMatrixHandler.handle({
|
|
1664
|
+
requirementsPath: parseFlag(args, "--requirements") ?? "docs/product/user_stories.md",
|
|
1665
|
+
testRoot: parseFlag(args, "--tests") ?? "scripts/harness/__tests__",
|
|
1666
|
+
matrixFilePath: parseFlag(args, "--out") ?? ".harness/requirement-test-matrix.json",
|
|
1667
|
+
}, flags);
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1655
1671
|
// ── ci-governance ──
|
|
1656
1672
|
case "ci:generate-template": {
|
|
1657
1673
|
if (hasFlag(args, "--help")) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @layer application
|
|
2
|
+
// @unit nyquist-validation
|
|
3
|
+
// @work-item-id WI-125
|
|
4
|
+
// @work-item-id WI-131
|
|
5
|
+
|
|
6
|
+
export interface RequirementSourceDto {
|
|
7
|
+
readonly storyId: string;
|
|
8
|
+
readonly acIds: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TestReferenceSourceDto {
|
|
12
|
+
readonly storyId: string;
|
|
13
|
+
readonly filePath: string;
|
|
14
|
+
readonly testType: 'unit' | 'it' | 'scenario';
|
|
15
|
+
readonly testName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MatrixTestReferenceDto {
|
|
19
|
+
readonly filePath: string;
|
|
20
|
+
readonly testType: 'unit' | 'it' | 'scenario';
|
|
21
|
+
readonly testName?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MatrixAcMappingDto {
|
|
25
|
+
readonly acId: string;
|
|
26
|
+
readonly testReferences: readonly MatrixTestReferenceDto[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MatrixStoryDto {
|
|
30
|
+
readonly storyId: string;
|
|
31
|
+
readonly storyMappings: readonly MatrixAcMappingDto[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RequirementTestMatrixDto {
|
|
35
|
+
readonly version: string;
|
|
36
|
+
readonly generatedAt: string;
|
|
37
|
+
readonly stories: readonly MatrixStoryDto[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MissingTestDto {
|
|
41
|
+
readonly storyId: string;
|
|
42
|
+
readonly acId: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface OrphanTestDto {
|
|
46
|
+
readonly storyId: string;
|
|
47
|
+
readonly filePath: string;
|
|
48
|
+
readonly testName?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type IntentCoverageStatus = 'observed' | 'weakly-observed' | 'unobserved';
|
|
52
|
+
|
|
53
|
+
export interface IntentCoverageItemDto {
|
|
54
|
+
readonly storyId: string;
|
|
55
|
+
readonly acId: string;
|
|
56
|
+
readonly status: IntentCoverageStatus;
|
|
57
|
+
readonly warnings: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MatrixGenerationReportDto {
|
|
61
|
+
readonly missingTests: readonly MissingTestDto[];
|
|
62
|
+
readonly orphanTests: readonly OrphanTestDto[];
|
|
63
|
+
readonly unknownStories: readonly string[];
|
|
64
|
+
readonly preservedReferences: number;
|
|
65
|
+
readonly intentCoverage: readonly IntentCoverageItemDto[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface GenerateMatrixOutput {
|
|
69
|
+
readonly matrix: RequirementTestMatrixDto;
|
|
70
|
+
readonly report: MatrixGenerationReportDto;
|
|
71
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// @layer application
|
|
2
|
+
// @unit nyquist-validation
|
|
3
|
+
// @work-item-id WI-125
|
|
4
|
+
// @work-item-id WI-131
|
|
5
|
+
|
|
6
|
+
import type { RequirementIntentCoverageService } from '../../domain/services/requirement-intent-coverage-service.js';
|
|
7
|
+
import type {
|
|
8
|
+
GenerateMatrixOutput,
|
|
9
|
+
MatrixAcMappingDto,
|
|
10
|
+
MatrixStoryDto,
|
|
11
|
+
MatrixTestReferenceDto,
|
|
12
|
+
RequirementSourceDto,
|
|
13
|
+
RequirementTestMatrixDto,
|
|
14
|
+
TestReferenceSourceDto,
|
|
15
|
+
} from '../dto/generate-matrix-output.js';
|
|
16
|
+
|
|
17
|
+
export interface RequirementSourcePort {
|
|
18
|
+
readRequirements(sourcePath: string): Promise<readonly RequirementSourceDto[]>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TestReferenceSourcePort {
|
|
22
|
+
readTestReferences(testRoot: string): Promise<readonly TestReferenceSourceDto[]>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ExistingMatrixPort {
|
|
26
|
+
readExistingMatrix(matrixFilePath: string): Promise<RequirementTestMatrixDto | null>;
|
|
27
|
+
writeMatrix(matrixFilePath: string, matrix: RequirementTestMatrixDto): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GenerateRequirementTestMatrixInput {
|
|
31
|
+
readonly requirementsPath: string;
|
|
32
|
+
readonly testRoot: string;
|
|
33
|
+
readonly matrixFilePath: string;
|
|
34
|
+
readonly write: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GenerateRequirementTestMatrixUseCaseDeps {
|
|
38
|
+
readonly requirementSourcePort: RequirementSourcePort;
|
|
39
|
+
readonly testReferenceSourcePort: TestReferenceSourcePort;
|
|
40
|
+
readonly matrixPort: ExistingMatrixPort;
|
|
41
|
+
readonly intentCoverageService: RequirementIntentCoverageService;
|
|
42
|
+
readonly now?: () => Date;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function referenceKey(reference: MatrixTestReferenceDto): string {
|
|
46
|
+
return `${reference.filePath}\0${reference.testType}\0${reference.testName ?? ''}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function mergeReferences(
|
|
50
|
+
generated: readonly MatrixTestReferenceDto[],
|
|
51
|
+
existing: readonly MatrixTestReferenceDto[],
|
|
52
|
+
): { references: readonly MatrixTestReferenceDto[]; preserved: number } {
|
|
53
|
+
const result = [...generated];
|
|
54
|
+
const keys = new Set(result.map(referenceKey));
|
|
55
|
+
let preserved = 0;
|
|
56
|
+
for (const reference of existing) {
|
|
57
|
+
if (!keys.has(referenceKey(reference))) {
|
|
58
|
+
result.push(reference);
|
|
59
|
+
keys.add(referenceKey(reference));
|
|
60
|
+
preserved += 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { references: Object.freeze(result), preserved };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeExistingReferences(
|
|
67
|
+
existingMatrix: RequirementTestMatrixDto | null,
|
|
68
|
+
storyId: string,
|
|
69
|
+
acId: string,
|
|
70
|
+
): readonly MatrixTestReferenceDto[] {
|
|
71
|
+
const story = existingMatrix?.stories.find((item) => item.storyId === storyId);
|
|
72
|
+
const mapping = story?.storyMappings.find((item) => item.acId === acId);
|
|
73
|
+
return mapping?.testReferences ?? [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class GenerateRequirementTestMatrixUseCase {
|
|
77
|
+
private readonly requirementSourcePort: RequirementSourcePort;
|
|
78
|
+
private readonly testReferenceSourcePort: TestReferenceSourcePort;
|
|
79
|
+
private readonly matrixPort: ExistingMatrixPort;
|
|
80
|
+
private readonly intentCoverageService: RequirementIntentCoverageService;
|
|
81
|
+
private readonly now: () => Date;
|
|
82
|
+
|
|
83
|
+
constructor(deps: GenerateRequirementTestMatrixUseCaseDeps) {
|
|
84
|
+
this.requirementSourcePort = deps.requirementSourcePort;
|
|
85
|
+
this.testReferenceSourcePort = deps.testReferenceSourcePort;
|
|
86
|
+
this.matrixPort = deps.matrixPort;
|
|
87
|
+
this.intentCoverageService = deps.intentCoverageService;
|
|
88
|
+
this.now = deps.now ?? (() => new Date());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async execute(input: GenerateRequirementTestMatrixInput): Promise<GenerateMatrixOutput> {
|
|
92
|
+
const [requirements, testReferences, existingMatrix] = await Promise.all([
|
|
93
|
+
this.requirementSourcePort.readRequirements(input.requirementsPath),
|
|
94
|
+
this.testReferenceSourcePort.readTestReferences(input.testRoot),
|
|
95
|
+
this.matrixPort.readExistingMatrix(input.matrixFilePath),
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
const knownStories = new Set(requirements.map((requirement) => requirement.storyId));
|
|
99
|
+
const orphanTests = testReferences
|
|
100
|
+
.filter((reference) => !knownStories.has(reference.storyId))
|
|
101
|
+
.map((reference) => ({
|
|
102
|
+
storyId: reference.storyId,
|
|
103
|
+
filePath: reference.filePath,
|
|
104
|
+
testName: reference.testName,
|
|
105
|
+
}));
|
|
106
|
+
const referencesByStory = new Map<string, TestReferenceSourceDto[]>();
|
|
107
|
+
for (const reference of testReferences) {
|
|
108
|
+
if (!knownStories.has(reference.storyId)) continue;
|
|
109
|
+
const current = referencesByStory.get(reference.storyId) ?? [];
|
|
110
|
+
current.push(reference);
|
|
111
|
+
referencesByStory.set(reference.storyId, current);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let preservedReferences = 0;
|
|
115
|
+
const stories: MatrixStoryDto[] = requirements.map((requirement) => {
|
|
116
|
+
const storyReferences = referencesByStory.get(requirement.storyId) ?? [];
|
|
117
|
+
const storyMappings: MatrixAcMappingDto[] = requirement.acIds.map((acId) => {
|
|
118
|
+
const generatedReferences = storyReferences.map((reference) => ({
|
|
119
|
+
filePath: reference.filePath,
|
|
120
|
+
testType: reference.testType,
|
|
121
|
+
testName: reference.testName,
|
|
122
|
+
}));
|
|
123
|
+
const existingReferences = normalizeExistingReferences(existingMatrix, requirement.storyId, acId);
|
|
124
|
+
const merged = mergeReferences(generatedReferences, existingReferences);
|
|
125
|
+
preservedReferences += merged.preserved;
|
|
126
|
+
return {
|
|
127
|
+
acId,
|
|
128
|
+
testReferences: merged.references,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
storyId: requirement.storyId,
|
|
133
|
+
storyMappings: Object.freeze(storyMappings),
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const matrix: RequirementTestMatrixDto = {
|
|
138
|
+
version: '1.0',
|
|
139
|
+
generatedAt: this.now().toISOString(),
|
|
140
|
+
stories: Object.freeze(stories),
|
|
141
|
+
};
|
|
142
|
+
const intentCoverage = this.intentCoverageService.evaluate(matrix.stories);
|
|
143
|
+
const missingTests = matrix.stories.flatMap((story) => story.storyMappings
|
|
144
|
+
.filter((mapping) => mapping.testReferences.length === 0)
|
|
145
|
+
.map((mapping) => ({ storyId: story.storyId, acId: mapping.acId })));
|
|
146
|
+
|
|
147
|
+
const report = {
|
|
148
|
+
missingTests,
|
|
149
|
+
orphanTests,
|
|
150
|
+
unknownStories: [...new Set(orphanTests.map((test) => test.storyId))],
|
|
151
|
+
preservedReferences,
|
|
152
|
+
intentCoverage,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (input.write) {
|
|
156
|
+
await this.matrixPort.writeMatrix(input.matrixFilePath, matrix);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { matrix, report };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* 全コンポーネントを生成・配線し、UseCase群を外部に公開する。
|
|
7
7
|
*/
|
|
8
8
|
import { FileSystemMatrixFileAdapter } from './infrastructure/adapters/file-system-matrix-file-adapter.js';
|
|
9
|
+
import { FileSystemGeneratedMatrixAdapter } from './infrastructure/adapters/file-system-generated-matrix-adapter.js';
|
|
10
|
+
import { MarkdownRequirementSourceAdapter } from './infrastructure/adapters/markdown-requirement-source-adapter.js';
|
|
11
|
+
import { TypeScriptTestReferenceSourceAdapter } from './infrastructure/adapters/type-script-test-reference-source-adapter.js';
|
|
9
12
|
import { AjvJsonSchemaValidatorAdapter } from './infrastructure/adapters/ajv-json-schema-validator-adapter.js';
|
|
10
13
|
import { ConfigFoundationCoverageThresholdAdapter } from './infrastructure/adapters/config-foundation-coverage-threshold-adapter.js';
|
|
11
14
|
import { TraceabilityModelStoryRegistryAdapter } from './infrastructure/adapters/traceability-model-story-registry-adapter.js';
|
|
@@ -13,14 +16,17 @@ import { MatrixValidationService } from './domain/services/matrix-validation-ser
|
|
|
13
16
|
import { AcCoverageGatePolicy } from './domain/services/ac-coverage-gate-policy.js';
|
|
14
17
|
import { CoverageCalculationService } from './domain/services/coverage-calculation-service.js';
|
|
15
18
|
import { ImpactAnalysisService } from './domain/services/impact-analysis-service.js';
|
|
19
|
+
import { RequirementIntentCoverageService } from './domain/services/requirement-intent-coverage-service.js';
|
|
16
20
|
import { ValidateMatrixUseCase } from './application/usecases/validate-matrix-usecase.js';
|
|
17
21
|
import { CheckAcCoverageGateUseCase } from './application/usecases/check-ac-coverage-gate-usecase.js';
|
|
18
22
|
import { CalculateCoverageUseCase } from './application/usecases/calculate-coverage-usecase.js';
|
|
19
23
|
import { AnalyzeImpactUseCase } from './application/usecases/analyze-impact-usecase.js';
|
|
24
|
+
import { GenerateRequirementTestMatrixUseCase } from './application/usecases/generate-requirement-test-matrix-usecase.js';
|
|
20
25
|
import { ValidateMatrixHandler } from './presentation/handlers/validate-matrix-handler.js';
|
|
21
26
|
import { CheckAcCoverageGateHandler } from './presentation/handlers/check-ac-coverage-gate-handler.js';
|
|
22
27
|
import { CalculateCoverageHandler } from './presentation/handlers/calculate-coverage-handler.js';
|
|
23
28
|
import { AnalyzeImpactHandler } from './presentation/handlers/analyze-impact-handler.js';
|
|
29
|
+
import { GenerateMatrixHandler } from './presentation/handlers/generate-matrix-handler.js';
|
|
24
30
|
|
|
25
31
|
export interface NyquistValidationModuleDeps {
|
|
26
32
|
/** traceability-model の storyCatalog.getAllStoryIds() を渡す */
|
|
@@ -32,6 +38,9 @@ export interface NyquistValidationModuleDeps {
|
|
|
32
38
|
export function createNyquistValidationModule(deps: NyquistValidationModuleDeps) {
|
|
33
39
|
// Infrastructure adapters
|
|
34
40
|
const matrixFilePort = new FileSystemMatrixFileAdapter();
|
|
41
|
+
const generatedMatrixPort = new FileSystemGeneratedMatrixAdapter();
|
|
42
|
+
const requirementSourcePort = new MarkdownRequirementSourceAdapter();
|
|
43
|
+
const testReferenceSourcePort = new TypeScriptTestReferenceSourceAdapter();
|
|
35
44
|
const ajvValidator = new AjvJsonSchemaValidatorAdapter();
|
|
36
45
|
const coverageThresholdPort = new ConfigFoundationCoverageThresholdAdapter({
|
|
37
46
|
getPreset: deps.getPreset,
|
|
@@ -45,6 +54,7 @@ export function createNyquistValidationModule(deps: NyquistValidationModuleDeps)
|
|
|
45
54
|
const acCoverageGatePolicy = new AcCoverageGatePolicy();
|
|
46
55
|
const coverageCalculationService = new CoverageCalculationService();
|
|
47
56
|
const impactAnalysisService = new ImpactAnalysisService();
|
|
57
|
+
const intentCoverageService = new RequirementIntentCoverageService();
|
|
48
58
|
|
|
49
59
|
// Application UseCases
|
|
50
60
|
const validateMatrixUseCase = new ValidateMatrixUseCase({
|
|
@@ -75,6 +85,13 @@ export function createNyquistValidationModule(deps: NyquistValidationModuleDeps)
|
|
|
75
85
|
impactAnalysisService,
|
|
76
86
|
});
|
|
77
87
|
|
|
88
|
+
const generateMatrixUseCase = new GenerateRequirementTestMatrixUseCase({
|
|
89
|
+
requirementSourcePort,
|
|
90
|
+
testReferenceSourcePort,
|
|
91
|
+
matrixPort: generatedMatrixPort,
|
|
92
|
+
intentCoverageService,
|
|
93
|
+
});
|
|
94
|
+
|
|
78
95
|
const validateMatrixHandler = new ValidateMatrixHandler({
|
|
79
96
|
validateMatrixUseCase,
|
|
80
97
|
});
|
|
@@ -91,18 +108,24 @@ export function createNyquistValidationModule(deps: NyquistValidationModuleDeps)
|
|
|
91
108
|
analyzeImpactUseCase,
|
|
92
109
|
});
|
|
93
110
|
|
|
111
|
+
const generateMatrixHandler = new GenerateMatrixHandler({
|
|
112
|
+
useCase: generateMatrixUseCase,
|
|
113
|
+
});
|
|
114
|
+
|
|
94
115
|
return {
|
|
95
116
|
validateMatrixUseCase,
|
|
96
117
|
checkAcCoverageGateUseCase,
|
|
97
118
|
calculateCoverageUseCase,
|
|
98
119
|
analyzeImpactUseCase,
|
|
120
|
+
generateMatrixUseCase,
|
|
99
121
|
handlers: {
|
|
100
122
|
validateMatrixHandler,
|
|
101
123
|
checkAcCoverageGateHandler,
|
|
102
124
|
calculateCoverageHandler,
|
|
103
125
|
analyzeImpactHandler,
|
|
126
|
+
generateMatrixHandler,
|
|
104
127
|
},
|
|
105
128
|
} as const;
|
|
106
129
|
}
|
|
107
130
|
|
|
108
|
-
// @story-id H08-07
|
|
131
|
+
// @story-id H08-07
|
package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @layer domain
|
|
2
|
+
// @unit nyquist-validation
|
|
3
|
+
// @work-item-id WI-131
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
IntentCoverageItemDto,
|
|
7
|
+
MatrixStoryDto,
|
|
8
|
+
} from '../../application/dto/generate-matrix-output.js';
|
|
9
|
+
|
|
10
|
+
export class RequirementIntentCoverageService {
|
|
11
|
+
evaluate(stories: readonly MatrixStoryDto[]): readonly IntentCoverageItemDto[] {
|
|
12
|
+
return stories.flatMap((story) => story.storyMappings.map((mapping) => {
|
|
13
|
+
if (mapping.testReferences.length === 0) {
|
|
14
|
+
return {
|
|
15
|
+
storyId: story.storyId,
|
|
16
|
+
acId: mapping.acId,
|
|
17
|
+
status: 'unobserved' as const,
|
|
18
|
+
warnings: ['No test reference observes this acceptance criterion.'],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hasNamedReference = mapping.testReferences.some((reference) => (
|
|
23
|
+
typeof reference.testName === 'string' && reference.testName.trim().length > 0
|
|
24
|
+
));
|
|
25
|
+
if (!hasNamedReference) {
|
|
26
|
+
return {
|
|
27
|
+
storyId: story.storyId,
|
|
28
|
+
acId: mapping.acId,
|
|
29
|
+
status: 'weakly-observed' as const,
|
|
30
|
+
warnings: ['Test reference exists, but testName is missing; observation intent is weak.'],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
storyId: story.storyId,
|
|
36
|
+
acId: mapping.acId,
|
|
37
|
+
status: 'weakly-observed' as const,
|
|
38
|
+
warnings: ['Test reference and testName exist, but assertion target / expected outcome evidence is not yet attached.'],
|
|
39
|
+
};
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -22,5 +22,13 @@ export type {
|
|
|
22
22
|
} from './presentation/handlers/calculate-coverage-handler.js';
|
|
23
23
|
export { AnalyzeImpactHandler } from './presentation/handlers/analyze-impact-handler.js';
|
|
24
24
|
export type { AnalyzeImpactHandlerArgs, AnalyzeImpactHandlerDeps } from './presentation/handlers/analyze-impact-handler.js';
|
|
25
|
+
export { GenerateMatrixHandler } from './presentation/handlers/generate-matrix-handler.js';
|
|
26
|
+
export { GenerateRequirementTestMatrixUseCase } from './application/usecases/generate-requirement-test-matrix-usecase.js';
|
|
27
|
+
export { RequirementIntentCoverageService } from './domain/services/requirement-intent-coverage-service.js';
|
|
28
|
+
export type {
|
|
29
|
+
GenerateMatrixOutput,
|
|
30
|
+
RequirementTestMatrixDto,
|
|
31
|
+
MatrixGenerationReportDto,
|
|
32
|
+
} from './application/dto/generate-matrix-output.js';
|
|
25
33
|
|
|
26
|
-
// @story-id H08-07
|
|
34
|
+
// @story-id H08-07
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @layer infrastructure
|
|
2
|
+
// @unit nyquist-validation
|
|
3
|
+
// @work-item-id WI-125
|
|
4
|
+
|
|
5
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import type { RequirementTestMatrixDto } from '../../application/dto/generate-matrix-output.js';
|
|
8
|
+
import type { ExistingMatrixPort } from '../../application/usecases/generate-requirement-test-matrix-usecase.js';
|
|
9
|
+
|
|
10
|
+
export class FileSystemGeneratedMatrixAdapter implements ExistingMatrixPort {
|
|
11
|
+
async readExistingMatrix(matrixFilePath: string): Promise<RequirementTestMatrixDto | null> {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(matrixFilePath, 'utf-8');
|
|
14
|
+
return JSON.parse(raw) as RequirementTestMatrixDto;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async writeMatrix(matrixFilePath: string, matrix: RequirementTestMatrixDto): Promise<void> {
|
|
21
|
+
await mkdir(path.dirname(matrixFilePath), { recursive: true });
|
|
22
|
+
await writeFile(matrixFilePath, `${JSON.stringify(matrix, null, 2)}\n`, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// @layer infrastructure
|
|
2
|
+
// @unit nyquist-validation
|
|
3
|
+
// @work-item-id WI-125
|
|
4
|
+
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import type { RequirementSourcePort } from '../../application/usecases/generate-requirement-test-matrix-usecase.js';
|
|
7
|
+
import type { RequirementSourceDto } from '../../application/dto/generate-matrix-output.js';
|
|
8
|
+
|
|
9
|
+
const STORY_HEADING = /^###\s+(H\d{2}-\d{2}):/;
|
|
10
|
+
const AC_ITEM = /^\s*-\s+\[[ xX]\]\s+(AC-[1-9][0-9]*):/;
|
|
11
|
+
|
|
12
|
+
export class MarkdownRequirementSourceAdapter implements RequirementSourcePort {
|
|
13
|
+
async readRequirements(sourcePath: string): Promise<readonly RequirementSourceDto[]> {
|
|
14
|
+
const content = await readFile(sourcePath, 'utf-8');
|
|
15
|
+
const results: RequirementSourceDto[] = [];
|
|
16
|
+
let current: { storyId: string; acIds: string[] } | null = null;
|
|
17
|
+
|
|
18
|
+
for (const line of content.split(/\r?\n/)) {
|
|
19
|
+
const storyMatch = line.match(STORY_HEADING);
|
|
20
|
+
if (storyMatch) {
|
|
21
|
+
if (current) {
|
|
22
|
+
results.push({ storyId: current.storyId, acIds: Object.freeze([...new Set(current.acIds)]) });
|
|
23
|
+
}
|
|
24
|
+
current = { storyId: storyMatch[1], acIds: [] };
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const acMatch = line.match(AC_ITEM);
|
|
28
|
+
if (current && acMatch) {
|
|
29
|
+
current.acIds.push(acMatch[1]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (current) {
|
|
33
|
+
results.push({ storyId: current.storyId, acIds: Object.freeze([...new Set(current.acIds)]) });
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze(results.filter((result) => result.acIds.length > 0));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @layer infrastructure
|
|
2
|
+
// @unit nyquist-validation
|
|
3
|
+
// @work-item-id WI-125
|
|
4
|
+
|
|
5
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import type { TestReferenceSourcePort } from '../../application/usecases/generate-requirement-test-matrix-usecase.js';
|
|
8
|
+
import type { TestReferenceSourceDto } from '../../application/dto/generate-matrix-output.js';
|
|
9
|
+
|
|
10
|
+
const STORY_TAG = /@story(?:-id)?\s+(H\d{2}-\d{2})/;
|
|
11
|
+
const TEST_NAME = /\b(?:it|test)(?:\.each\([^)]*\))?\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
12
|
+
|
|
13
|
+
async function collectTestFiles(root: string): Promise<readonly string[]> {
|
|
14
|
+
const rootStat = await stat(root).catch(() => null);
|
|
15
|
+
if (rootStat === null) return [];
|
|
16
|
+
if (rootStat.isFile()) return /\.(test|spec)\.[cm]?[tj]sx?$/.test(root) ? [root] : [];
|
|
17
|
+
|
|
18
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
19
|
+
const nested = await Promise.all(entries
|
|
20
|
+
.filter((entry) => entry.name !== 'node_modules' && entry.name !== 'dist' && entry.name !== '.git')
|
|
21
|
+
.map((entry) => collectTestFiles(path.join(root, entry.name))));
|
|
22
|
+
return Object.freeze(nested.flat());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function classifyTestType(filePath: string): 'unit' | 'it' | 'scenario' {
|
|
26
|
+
const normalized = filePath.toLowerCase();
|
|
27
|
+
if (normalized.includes('/scenario/') || normalized.includes('.scenario.')) return 'scenario';
|
|
28
|
+
if (normalized.includes('/integration/') || normalized.includes('.it.') || normalized.includes('.integration.')) return 'it';
|
|
29
|
+
return 'unit';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class TypeScriptTestReferenceSourceAdapter implements TestReferenceSourcePort {
|
|
33
|
+
async readTestReferences(testRoot: string): Promise<readonly TestReferenceSourceDto[]> {
|
|
34
|
+
const files = await collectTestFiles(testRoot);
|
|
35
|
+
const references: TestReferenceSourceDto[] = [];
|
|
36
|
+
for (const filePath of files) {
|
|
37
|
+
const content = await readFile(filePath, 'utf-8');
|
|
38
|
+
const storyMatch = content.match(STORY_TAG);
|
|
39
|
+
if (!storyMatch) continue;
|
|
40
|
+
const storyId = storyMatch[1];
|
|
41
|
+
const relativePath = path.relative(process.cwd(), filePath).replaceAll(path.sep, '/');
|
|
42
|
+
const testType = classifyTestType(relativePath);
|
|
43
|
+
const names = [...content.matchAll(TEST_NAME)].map((match) => match[1]);
|
|
44
|
+
if (names.length === 0) {
|
|
45
|
+
references.push({ storyId, filePath: relativePath, testType });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const testName of names) {
|
|
49
|
+
references.push({ storyId, filePath: relativePath, testType, testName });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze(references);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @layer presentation
|
|
2
|
+
// @unit nyquist-validation
|
|
3
|
+
// @work-item-id WI-125
|
|
4
|
+
// @work-item-id WI-131
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
GenerateRequirementTestMatrixInput,
|
|
8
|
+
GenerateRequirementTestMatrixUseCase,
|
|
9
|
+
} from '../../application/usecases/generate-requirement-test-matrix-usecase.js';
|
|
10
|
+
|
|
11
|
+
export interface GenerateMatrixHandlerDeps {
|
|
12
|
+
readonly useCase: GenerateRequirementTestMatrixUseCase;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class GenerateMatrixHandler {
|
|
16
|
+
private readonly useCase: GenerateRequirementTestMatrixUseCase;
|
|
17
|
+
|
|
18
|
+
constructor(deps: GenerateMatrixHandlerDeps) {
|
|
19
|
+
this.useCase = deps.useCase;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async handle(input: Omit<GenerateRequirementTestMatrixInput, 'write'>, flags: Record<string, boolean | string>): Promise<void> {
|
|
23
|
+
const output = await this.useCase.execute({ ...input, write: true });
|
|
24
|
+
if (flags.json === true) {
|
|
25
|
+
console.log(JSON.stringify(output, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(`Requirement test matrix generated: ${input.matrixFilePath}`);
|
|
29
|
+
console.log(`Stories: ${output.matrix.stories.length}`);
|
|
30
|
+
console.log(`Missing tests: ${output.report.missingTests.length}`);
|
|
31
|
+
console.log(`Orphan tests: ${output.report.orphanTests.length}`);
|
|
32
|
+
console.log(`Preserved references: ${output.report.preservedReferences}`);
|
|
33
|
+
}
|
|
34
|
+
}
|