phasegate 0.149.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.ja.md +7 -6
  3. package/README.md +7 -6
  4. package/docs/guide/cli-reference.md +15 -2
  5. package/docs/guide/installation.md +1 -1
  6. package/docs/guide/layer-model.md +11 -2
  7. package/docs/guide/quick-vs-full-mode.md +1 -1
  8. package/docs/guide/retrofit-adoption.md +1 -1
  9. package/docs/guide/skills-overview.md +8 -1
  10. package/docs/templates/ci/consistency-check.yml +6 -6
  11. package/package.json +1 -1
  12. package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +31 -0
  13. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +7 -0
  14. package/scripts/harness/agent-integration/presentation/stop-hook.ts +7 -0
  15. package/scripts/harness/ci-governance/domain/ports/validator-id-registry-port.ts +1 -0
  16. package/scripts/harness/ci-governance/domain/services/template-generator.ts +2 -1
  17. package/scripts/harness/ci-governance/infrastructure/adapters/validator-id-registry-adapter.ts +31 -3
  18. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +48 -1
  19. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +7 -1
  20. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +43 -0
  21. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +91 -1
  22. package/scripts/harness/main.ts +16 -0
  23. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +71 -0
  24. package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +161 -0
  25. package/scripts/harness/nyquist-validation/composition-root.ts +24 -1
  26. package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +42 -0
  27. package/scripts/harness/nyquist-validation/index.ts +9 -1
  28. package/scripts/harness/nyquist-validation/infrastructure/adapters/file-system-generated-matrix-adapter.ts +24 -0
  29. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +37 -0
  30. package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +54 -0
  31. package/scripts/harness/nyquist-validation/presentation/handlers/generate-matrix-handler.ts +34 -0
  32. package/scripts/harness/validator-system/composition-root.ts +1 -1
@@ -1,10 +1,13 @@
1
1
  // @layer infrastructure
2
2
  // @unit harness-api
3
+ // @work-item-id WI-123
3
4
  // harness-config-query-adapter.ts — HarnessConfigQueryAdapter
4
5
 
5
6
  import * as fs from 'node:fs/promises';
7
+ import { createHash } from 'node:crypto';
8
+ import { dirname, join, resolve } from 'node:path';
6
9
  import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
7
- import type { PresetInfo, ConfigSummary, PhaseGateSummary } from '../../domain/value-objects/harness-status-summary.js';
10
+ import type { PresetInfo, ConfigSummary, PhaseGateSummary, HookHealth, BaselineHealth } from '../../domain/value-objects/harness-status-summary.js';
8
11
  import type { LayerId } from '../../domain/value-objects/layer-health.js';
9
12
 
10
13
  interface HarnessConfigJson {
@@ -18,6 +21,10 @@ interface HarnessConfigJson {
18
21
  designDocs?: string;
19
22
  integrationTests?: string;
20
23
  };
24
+ baseline?: {
25
+ enabled?: boolean;
26
+ path?: string;
27
+ };
21
28
  }
22
29
 
23
30
  const PRESET_LAYERS: Record<string, LayerId[]> = {
@@ -77,4 +84,87 @@ export class HarnessConfigQueryAdapter implements ConfigQueryPort {
77
84
  // wave2-pending: integrate with phase-dependency-model
78
85
  return { totalStories: 0, passedStories: 0, pendingStories: 0 };
79
86
  }
87
+
88
+ async getHookHealth(): Promise<HookHealth> {
89
+ const rootDir = dirname(this.configPath);
90
+ const configuredHooks: string[] = [];
91
+ for (const hookPath of ['.claude/settings.json', '.codex/hooks.json', '.husky/pre-commit', '.husky/pre-push']) {
92
+ try {
93
+ await fs.access(join(rootDir, hookPath));
94
+ configuredHooks.push(hookPath);
95
+ } catch {
96
+ // Missing hook files are represented by absence from configuredHooks.
97
+ }
98
+ }
99
+
100
+ const skipEvents = await readHookSkipEvents(join(rootDir, '.phasegate/hook-skip-events.jsonl'));
101
+ const skipCountsByReason: Record<string, number> = {};
102
+ for (const event of skipEvents) {
103
+ skipCountsByReason[event.reason] = (skipCountsByReason[event.reason] ?? 0) + 1;
104
+ }
105
+
106
+ return {
107
+ enabled: configuredHooks.length > 0,
108
+ configuredHooks,
109
+ latestSkip: skipEvents.at(-1) ?? null,
110
+ skipCountsByReason,
111
+ applyPatchBypass: {
112
+ nativeApplyPatchIntercepted: false,
113
+ backstop: 'pre-commit',
114
+ documentationUrl: 'docs/guide/codex-integration.md',
115
+ },
116
+ };
117
+ }
118
+
119
+ async getBaselineHealth(): Promise<BaselineHealth> {
120
+ const config = await this.readConfig();
121
+ const rootDir = dirname(this.configPath);
122
+ const enabled = config.baseline?.enabled ?? true;
123
+ const relativePath = config.baseline?.path ?? '.phasegate/baseline.json';
124
+ const baselinePath = resolve(rootDir, relativePath);
125
+
126
+ if (!enabled) {
127
+ return { enabled: false, path: relativePath, grandfatheredFileCount: 0, shaMismatchCount: 0, missingFileCount: 0, removalRate: 0 };
128
+ }
129
+
130
+ let raw: string;
131
+ try {
132
+ raw = await fs.readFile(baselinePath, 'utf-8');
133
+ } catch {
134
+ return { enabled: true, path: relativePath, grandfatheredFileCount: 0, shaMismatchCount: 0, missingFileCount: 0, removalRate: 0 };
135
+ }
136
+
137
+ const parsed = JSON.parse(raw) as { files?: Array<{ path?: string; sha1?: string }>; entries?: Array<{ path?: string; sha1?: string }> };
138
+ const entries = parsed.files ?? parsed.entries ?? [];
139
+ let shaMismatchCount = 0;
140
+ let missingFileCount = 0;
141
+ for (const entry of entries) {
142
+ if (typeof entry.path !== 'string' || typeof entry.sha1 !== 'string') continue;
143
+ try {
144
+ const current = await fs.readFile(resolve(rootDir, entry.path));
145
+ const sha1 = createHash('sha1').update(current).digest('hex');
146
+ if (sha1 !== entry.sha1) shaMismatchCount += 1;
147
+ } catch {
148
+ missingFileCount += 1;
149
+ }
150
+ }
151
+ const grandfatheredFileCount = entries.length;
152
+ const removalRate = grandfatheredFileCount === 0
153
+ ? 1
154
+ : (shaMismatchCount + missingFileCount) / grandfatheredFileCount;
155
+ return { enabled: true, path: relativePath, grandfatheredFileCount, shaMismatchCount, missingFileCount, removalRate };
156
+ }
157
+ }
158
+
159
+ async function readHookSkipEvents(filePath: string): Promise<Array<NonNullable<HookHealth['latestSkip']>>> {
160
+ try {
161
+ const raw = await fs.readFile(filePath, 'utf-8');
162
+ return raw
163
+ .split('\n')
164
+ .filter((line) => line.trim().length > 0)
165
+ .map((line) => JSON.parse(line) as HookHealth['latestSkip'])
166
+ .filter((event): event is NonNullable<HookHealth['latestSkip']> => event !== null);
167
+ } catch {
168
+ return [];
169
+ }
80
170
  }
@@ -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
+ }