phasegate 0.150.0 → 0.150.2

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.ja.md +1 -1
  3. package/README.md +1 -1
  4. package/docs/guide/cli-reference.md +13 -0
  5. package/docs/guide/layer-model.md +4 -0
  6. package/package.json +1 -1
  7. package/scripts/harness/main.ts +16 -0
  8. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +71 -0
  9. package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +161 -0
  10. package/scripts/harness/nyquist-validation/composition-root.ts +24 -1
  11. package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +42 -0
  12. package/scripts/harness/nyquist-validation/index.ts +9 -1
  13. package/scripts/harness/nyquist-validation/infrastructure/adapters/file-system-generated-matrix-adapter.ts +24 -0
  14. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +37 -0
  15. package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +54 -0
  16. package/scripts/harness/nyquist-validation/presentation/handlers/generate-matrix-handler.ts +34 -0
  17. package/scripts/harness/phase2-extensions/application/dto/validate-doc-pointers-output.ts +4 -0
  18. package/scripts/harness/phase2-extensions/application/usecases/validate-doc-pointers-usecase.ts +23 -4
  19. package/scripts/harness/phase2-extensions/domain/aggregates/pointer-rule.ts +13 -0
  20. package/scripts/harness/phase2-extensions/domain/services/freshness-check-service.ts +10 -0
  21. package/scripts/harness/phase2-extensions/domain/value-objects/document-age.ts +2 -2
  22. package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +12 -0
  23. package/scripts/harness/phase2-extensions/presentation/formatters/pointer-result-formatter.ts +3 -1
  24. package/scripts/harness/validator-system/domain/services/l4/consistency-check-service.ts +15 -17
  25. package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +95 -51
  26. package/scripts/harness/validator-system/domain/services/l4/semantic-drift-service.ts +112 -0
  27. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +2 -1
  28. package/scripts/harness/validator-system/domain/value-objects/semantic-drift-report.ts +50 -0
  29. package/scripts/harness/validator-system/index.ts +2 -0
  30. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +30 -2
  31. package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts +110 -20
package/CHANGELOG.md CHANGED
@@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.150.2] - 2026-05-12
11
+
12
+ ### Added
13
+
14
+ - **G3 / WI-117 / WI-118 / WI-122 / WI-139 — L4 drift, consistency, docs, and semantic drift semantics** — L4 drift detection now compares unit-scoped records, prefers `@unit` metadata, handles product construction docs and re-export/default export surfaces more precisely, reports real product-doc consistency targets, adds operational pointer/freshness semantics, and introduces semantic drift reports across design intent, implementation behavior, and test observations.
15
+
16
+ ## [0.150.1] - 2026-05-12
17
+
18
+ ### Fixed
19
+
20
+ - **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.
21
+
10
22
  ## [0.150.0] - 2026-05-12
11
23
 
12
24
  ### Added
13
25
 
14
26
  - **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.
27
+ - **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
28
 
16
29
  ## [0.147.0] - 2026-05-12
17
30
 
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` 自動生成はまだ未自動化です。[WI-125](docs/inception/_cross/WI-125/description.md) L3 guide を参照してください。現時点では手動セットアップで利用できます。
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` auto-generation for L3 Nyquist Validation is not automated yet; see [WI-125](docs/inception/_cross/WI-125/description.md) and the L3 guide for the current manual setup.
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.
@@ -253,3 +253,7 @@ Presets control which layers are active. Choose based on project maturity and te
253
253
  - **strict** — Full defense. Enables scheduled drift detection (L4). Runtime L0 hooks are configured through the agent and Husky hook files, not through validator presets.
254
254
 
255
255
  Presets are configured in `phasegate.config.json`, the single source of truth for all quality settings.
256
+ <!-- @work-item-id WI-117, WI-118, WI-122, WI-139 -->
257
+ ## G3 L4 Advisory Preconditions
258
+
259
+ L4 drift / consistency / docs semantic findings are advisory by default. Use fail-on-warning only when reports include Unit-scoped drift keys, real product-doc consistency targets, pointer/freshness owner and policy metadata, and semantic drift behavior coverage. This avoids treating parser limitations as blocking quality gates.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.150.0",
3
+ "version": "0.150.2",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -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
@@ -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
+ }
@@ -8,8 +8,12 @@ export interface PointerValidationResultDto {
8
8
  documentPath: string;
9
9
  pointerTarget: string;
10
10
  pointerType: 'file-path' | 'url';
11
+ semanticPointerType: 'reference' | 'implementation' | 'adr' | 'product-doc' | 'external-url';
12
+ owner: string;
13
+ severity: 'fail' | 'warn' | 'skip';
11
14
  isResolvable: boolean;
12
15
  errorMessage: string | null;
16
+ nextAction: string;
13
17
  }
14
18
 
15
19
  export interface ValidateDocPointersOutput {