phasegate 0.165.0 → 0.167.0
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 +19 -0
- package/docs/contracts/requirement-test-matrix.schema.json +5 -0
- package/package.json +1 -1
- package/scripts/harness/ci-governance/infrastructure/adapters/validator-id-registry-adapter.ts +8 -0
- package/scripts/harness/main.ts +2 -2
- package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +42 -0
- package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +62 -7
- package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +110 -8
- package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +5 -0
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +23 -0
- package/scripts/harness/validator-system/composition-root.ts +10 -0
- package/scripts/harness/validator-system/domain/ports/ac-level-traceability-port.ts +14 -0
- package/scripts/harness/validator-system/domain/services/l4/ac-level-traceability-service.ts +102 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +3 -0
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-level-traceability-adapter.ts +104 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.167.0] - 2026-07-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **WI-222 / HF2-05 — AC-level traceability (`@ac` contract, additive)** — adds a mechanical (AST-free, ADR-019-compliant) `@ac` annotation contract so individual test cases can record which acceptance criterion they assert. The `TypeScriptTestReferenceSourceAdapter` parses absolute (`// @ac H05-02-1`), story-relative (`// @ac AC-2`, resolved only when the file has exactly one `@story`), and multi-AC (`// @ac H05-02-1 H05-02-2`) forms via a positional scan that binds the nearest preceding `@ac` since the last `it(`/`test(`. New DTO fields: `TestReferenceSourceDto.acIds`, `MatrixTestReferenceDto.binding` (`"ac" | "file"`), report `acLevelCoverage` (`{ total, acBound, fileFallbackOnly }`) and `orphanAcTags`.
|
|
15
|
+
- **WI-222 / HF2-05 — L4-007 `ac-level-traceability` advisory validator (`validator-system` unit)** — a new L4 validator that surfaces file-fallback-only ACs and orphan `@ac` tags as **warning-severity findings only (never errors)**. It is **default-OFF** (registered in the registry but not in the default/standard/strict enabled validator sets, so it always `skipped=true` in `ci-check`), **advisory/non-blocking**, and **attestation-trust-excluded** (the attestation `GranularityDerivationService` re-derives granularity solely from the static `KNOWN_LIMITATIONS_REGISTRY`, which whitelists only L3-004; L4-007 is ignored by construction). New domain service `AcLevelTraceabilityService`, port `AcLevelTraceabilityPort`, and infrastructure adapter `NyquistAcLevelTraceabilityAdapter`.
|
|
16
|
+
- **requirement-test-matrix schema 1.1** — adds an optional `binding` enum (`["ac","file"]`) on `testReferences`, keeping `additionalProperties:false` and full backward compatibility so existing 1.0 matrices (without `binding`) still validate.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **L3-004 verdict preserved (byte-identical)** — references without `@ac` continue to fan out to all ACs with `binding:"file"` exactly as before; the reference dedup key now incorporates `binding` and normalizes `undefined → "file"` so 1.0 matrices without `binding` produce no spurious duplicates. Proven invariant by golden-matrix comparison (all 358 pre-existing ACs: identical coverage, 0 missing before/after). Generated matrix `version` bumped `1.0 → 1.1`.
|
|
21
|
+
- **Attestation granularity unchanged** — `granularity.traceability.level` remains `"file"`; `level:"ac"` is design-gated on a future fail-closed **L3-005 "AC-bound coverage"** validator flipping the static registry binary/global (documented in `nyquist-validation/logical_design.md` and the L3-004 ratchet R5 row).
|
|
22
|
+
|
|
23
|
+
## [0.166.0] - 2026-07-05
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **WI-220 — noUselessTernary dogfooding fix (`harness-api` unit, `scripts/harness/main.ts`)** — replaces two `lint/complexity/noUselessTernary` violations in the `setup` install path (`includeHusky: personal ? false : true` / `includeCi: personal ? false : true`) with the behavior-preserving `!personal`. Clears self-repo biome lint debt; `npx biome lint scripts/harness/main.ts` now reports 0 errors. No behavior change — `!personal` is semantically identical to `personal ? false : true`.
|
|
28
|
+
|
|
10
29
|
## [0.165.0] - 2026-07-05
|
|
11
30
|
|
|
12
31
|
### Added
|
package/package.json
CHANGED
package/scripts/harness/ci-governance/infrastructure/adapters/validator-id-registry-adapter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @unit ci-governance
|
|
2
2
|
// @layer infrastructure
|
|
3
3
|
// @work-item-id WI-124 / WI-128
|
|
4
|
+
// @work-item-id WI-222
|
|
4
5
|
|
|
5
6
|
import type { ValidatorIdRegistryPort } from '../../domain/ports/validator-id-registry-port.js';
|
|
6
7
|
import { buildDefaultRegistry } from '../../../validator-system/composition-root.js';
|
|
@@ -8,6 +9,11 @@ import type { ValidatorDefinition } from '../../../validator-system/domain/value
|
|
|
8
9
|
|
|
9
10
|
type PresetId = 'minimal' | 'standard' | 'strict';
|
|
10
11
|
|
|
12
|
+
// WI-222 / HF2-05: default-OFF な advisory-only バリデータ。scheduled audit metadata や
|
|
13
|
+
// preset 導出リストには含めない(default-OFF の不変条件を保つ)。runtime では
|
|
14
|
+
// DEFAULT_CONFIG.layers.L4.validators に含まれないことで skip される。
|
|
15
|
+
const ADVISORY_DEFAULT_OFF_IDS = new Set<string>(['L4-007']);
|
|
16
|
+
|
|
11
17
|
export class ValidatorIdRegistryAdapter implements ValidatorIdRegistryPort {
|
|
12
18
|
constructor(private readonly registryUrl?: string) {}
|
|
13
19
|
|
|
@@ -30,6 +36,8 @@ function normalizePreset(presetId: string): PresetId {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
function isIncludedForPreset(definition: ValidatorDefinition, preset: PresetId, templateType: string): boolean {
|
|
39
|
+
// WI-222 / HF2-05: default-OFF advisory バリデータは全 preset で除外する。
|
|
40
|
+
if (ADVISORY_DEFAULT_OFF_IDS.has(definition.validatorId.value)) return false;
|
|
33
41
|
if (preset === 'minimal') return false;
|
|
34
42
|
if (definition.layer === 'L4') {
|
|
35
43
|
if (templateType === 'consistency-check') return true;
|
package/scripts/harness/main.ts
CHANGED
|
@@ -2150,8 +2150,8 @@ async function main(): Promise<void> {
|
|
|
2150
2150
|
force: hasFlag(args, "--force"),
|
|
2151
2151
|
includeClaude,
|
|
2152
2152
|
includeCodex,
|
|
2153
|
-
includeHusky: personal
|
|
2154
|
-
includeCi: personal
|
|
2153
|
+
includeHusky: !personal,
|
|
2154
|
+
includeCi: !personal,
|
|
2155
2155
|
skillSet: skillSetRaw,
|
|
2156
2156
|
workflow: parseWorkflowMode(workflowRaw),
|
|
2157
2157
|
agent,
|
|
@@ -15,17 +15,43 @@ export interface RequirementSourceDto {
|
|
|
15
15
|
readonly acIds: readonly string[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* HF2-05: @ac が story 外の AC を指した/複数 @story のため相対 AC が解決できなかった等の
|
|
20
|
+
* 「解決に失敗した @ac タグ」を advisory として記録する(error ではない)。
|
|
21
|
+
*/
|
|
22
|
+
export interface OrphanAcTagDto {
|
|
23
|
+
readonly storyId: string;
|
|
24
|
+
readonly filePath: string;
|
|
25
|
+
readonly testName?: string;
|
|
26
|
+
/** 解決できなかった生の @ac 表記(絶対 HXX-YY-N / 相対 AC-N のいずれか) */
|
|
27
|
+
readonly rawTag: string;
|
|
28
|
+
readonly reason: 'ac-not-in-story' | 'relative-multi-story';
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export interface TestReferenceSourceDto {
|
|
19
32
|
readonly storyId: string;
|
|
20
33
|
readonly filePath: string;
|
|
21
34
|
readonly testType: 'unit' | 'it' | 'scenario';
|
|
22
35
|
readonly testName?: string;
|
|
36
|
+
/**
|
|
37
|
+
* HF2-05: このテストケースが個別に検証する AC の id 群(`AC-N` 形式)。
|
|
38
|
+
* @ac 注釈が無いテストは undefined(従来どおり file-fallback で全 AC にファンアウトする)。
|
|
39
|
+
*/
|
|
40
|
+
readonly acIds?: readonly string[];
|
|
41
|
+
/** HF2-05: 解決に失敗した @ac タグ(advisory)。 */
|
|
42
|
+
readonly orphanAcTags?: readonly OrphanAcTagDto[];
|
|
23
43
|
}
|
|
24
44
|
|
|
25
45
|
export interface MatrixTestReferenceDto {
|
|
26
46
|
readonly filePath: string;
|
|
27
47
|
readonly testType: 'unit' | 'it' | 'scenario';
|
|
28
48
|
readonly testName?: string;
|
|
49
|
+
/**
|
|
50
|
+
* HF2-05: この参照が AC 単位で紐づいたか(`"ac"`)、
|
|
51
|
+
* @ac 注釈が無く file-fallback で全 AC にファンアウトしたか(`"file"`)。
|
|
52
|
+
* 1.0 マトリクス(binding 無し)は `"file"` と等価に扱う(dedup 正規化)。
|
|
53
|
+
*/
|
|
54
|
+
readonly binding?: 'ac' | 'file';
|
|
29
55
|
}
|
|
30
56
|
|
|
31
57
|
export interface MatrixAcMappingDto {
|
|
@@ -59,12 +85,28 @@ export type IntentCoverageStatus = DomainIntentCoverageStatus;
|
|
|
59
85
|
|
|
60
86
|
export type IntentCoverageItemDto = DomainIntentCoverageItem;
|
|
61
87
|
|
|
88
|
+
/**
|
|
89
|
+
* HF2-05: AC 単位トレーサビリティの advisory 集計。
|
|
90
|
+
* - total: マトリクス上の全 AC 数
|
|
91
|
+
* - acBound: 少なくとも 1 件の binding="ac" 参照を持つ AC 数
|
|
92
|
+
* - fileFallbackOnly: binding="ac" 参照を持たない(file-fallback のみで linked な)AC 数
|
|
93
|
+
*/
|
|
94
|
+
export interface AcLevelCoverageDto {
|
|
95
|
+
readonly total: number;
|
|
96
|
+
readonly acBound: number;
|
|
97
|
+
readonly fileFallbackOnly: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
62
100
|
export interface MatrixGenerationReportDto {
|
|
63
101
|
readonly missingTests: readonly MissingTestDto[];
|
|
64
102
|
readonly orphanTests: readonly OrphanTestDto[];
|
|
65
103
|
readonly unknownStories: readonly string[];
|
|
66
104
|
readonly preservedReferences: number;
|
|
67
105
|
readonly intentCoverage: readonly IntentCoverageItemDto[];
|
|
106
|
+
/** HF2-05: AC 単位カバレッジ集計(advisory)。 */
|
|
107
|
+
readonly acLevelCoverage: AcLevelCoverageDto;
|
|
108
|
+
/** HF2-05: 解決に失敗した @ac タグ(advisory)。 */
|
|
109
|
+
readonly orphanAcTags: readonly OrphanAcTagDto[];
|
|
68
110
|
}
|
|
69
111
|
|
|
70
112
|
export interface GenerateMatrixOutput {
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import type { RequirementIntentCoverageService } from '../../domain/services/requirement-intent-coverage-service.js';
|
|
7
7
|
import type {
|
|
8
|
+
AcLevelCoverageDto,
|
|
8
9
|
GenerateMatrixOutput,
|
|
9
10
|
MatrixAcMappingDto,
|
|
10
11
|
MatrixStoryDto,
|
|
11
12
|
MatrixTestReferenceDto,
|
|
13
|
+
OrphanAcTagDto,
|
|
12
14
|
RequirementSourceDto,
|
|
13
15
|
RequirementTestMatrixDto,
|
|
14
16
|
TestReferenceSourceDto,
|
|
@@ -42,8 +44,12 @@ export interface GenerateRequirementTestMatrixUseCaseDeps {
|
|
|
42
44
|
readonly now?: () => Date;
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
// HF2-05: dedup キーは binding を含める。1.0 マトリクス(binding undefined)は
|
|
48
|
+
// "file" に正規化するため、binding を持たない既存参照と file-fallback 生成参照が
|
|
49
|
+
// 同一キーに畳まれ、意図しない重複(=L3-004 の testReferences 件数変化)を防ぐ。
|
|
45
50
|
function referenceKey(reference: MatrixTestReferenceDto): string {
|
|
46
|
-
|
|
51
|
+
const binding = reference.binding ?? 'file';
|
|
52
|
+
return `${reference.filePath}\0${reference.testType}\0${reference.testName ?? ''}\0${binding}`;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
function mergeReferences(
|
|
@@ -112,30 +118,77 @@ export class GenerateRequirementTestMatrixUseCase {
|
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
let preservedReferences = 0;
|
|
121
|
+
// HF2-05: 各 AC の ac-bound 判定を集計するためのアキュムレータ。
|
|
122
|
+
let acLevelTotal = 0;
|
|
123
|
+
let acLevelBound = 0;
|
|
124
|
+
let acLevelFileFallbackOnly = 0;
|
|
125
|
+
const orphanAcTags: OrphanAcTagDto[] = [];
|
|
115
126
|
const stories: MatrixStoryDto[] = requirements.map((requirement) => {
|
|
116
127
|
const storyReferences = referencesByStory.get(requirement.storyId) ?? [];
|
|
117
128
|
const storyMappings: MatrixAcMappingDto[] = requirement.acIds.map((acId) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
// HF2-05:
|
|
130
|
+
// - acIds を持つ参照は、その acId を含むときだけ binding="ac" で紐づく(ファンアウトしない)。
|
|
131
|
+
// - acIds を持たない参照は従来どおり全 AC へ binding="file" でファンアウトする(L3-004 不変)。
|
|
132
|
+
const generatedReferences: MatrixTestReferenceDto[] = [];
|
|
133
|
+
let hasAcBound = false;
|
|
134
|
+
for (const reference of storyReferences) {
|
|
135
|
+
const acIds = reference.acIds;
|
|
136
|
+
if (acIds && acIds.length > 0) {
|
|
137
|
+
if (acIds.includes(acId)) {
|
|
138
|
+
generatedReferences.push({
|
|
139
|
+
filePath: reference.filePath,
|
|
140
|
+
testType: reference.testType,
|
|
141
|
+
testName: reference.testName,
|
|
142
|
+
binding: 'ac',
|
|
143
|
+
});
|
|
144
|
+
hasAcBound = true;
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
generatedReferences.push({
|
|
149
|
+
filePath: reference.filePath,
|
|
150
|
+
testType: reference.testType,
|
|
151
|
+
testName: reference.testName,
|
|
152
|
+
binding: 'file',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
123
155
|
const existingReferences = normalizeExistingReferences(existingMatrix, requirement.storyId, acId);
|
|
124
156
|
const merged = mergeReferences(generatedReferences, existingReferences);
|
|
125
157
|
preservedReferences += merged.preserved;
|
|
158
|
+
// AC 単位カバレッジ集計(advisory): linked(参照 1 件以上)な AC のみ分母に数える。
|
|
159
|
+
if (merged.references.length > 0) {
|
|
160
|
+
acLevelTotal += 1;
|
|
161
|
+
if (hasAcBound) {
|
|
162
|
+
acLevelBound += 1;
|
|
163
|
+
} else {
|
|
164
|
+
acLevelFileFallbackOnly += 1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
126
167
|
return {
|
|
127
168
|
acId,
|
|
128
169
|
testReferences: merged.references,
|
|
129
170
|
};
|
|
130
171
|
});
|
|
172
|
+
// 解決に失敗した @ac タグを advisory として集約する。
|
|
173
|
+
for (const reference of storyReferences) {
|
|
174
|
+
if (reference.orphanAcTags) orphanAcTags.push(...reference.orphanAcTags);
|
|
175
|
+
}
|
|
131
176
|
return {
|
|
132
177
|
storyId: requirement.storyId,
|
|
133
178
|
storyMappings: Object.freeze(storyMappings),
|
|
134
179
|
};
|
|
135
180
|
});
|
|
181
|
+
const acLevelCoverage: AcLevelCoverageDto = {
|
|
182
|
+
total: acLevelTotal,
|
|
183
|
+
acBound: acLevelBound,
|
|
184
|
+
fileFallbackOnly: acLevelFileFallbackOnly,
|
|
185
|
+
};
|
|
136
186
|
|
|
137
187
|
const matrix: RequirementTestMatrixDto = {
|
|
138
|
-
|
|
188
|
+
// HF2-05: binding フィールドを付与するため schema 1.1 として出力する。
|
|
189
|
+
// 1.1 は 1.0 に対し optional な binding のみ追加であり、L3-004 の判定(各 AC に
|
|
190
|
+
// testReference が 1 件以上あるか)には一切影響しない。
|
|
191
|
+
version: '1.1',
|
|
139
192
|
generatedAt: this.now().toISOString(),
|
|
140
193
|
stories: Object.freeze(stories),
|
|
141
194
|
};
|
|
@@ -150,6 +203,8 @@ export class GenerateRequirementTestMatrixUseCase {
|
|
|
150
203
|
unknownStories: [...new Set(orphanTests.map((test) => test.storyId))],
|
|
151
204
|
preservedReferences,
|
|
152
205
|
intentCoverage,
|
|
206
|
+
acLevelCoverage,
|
|
207
|
+
orphanAcTags: Object.freeze(orphanAcTags),
|
|
153
208
|
};
|
|
154
209
|
|
|
155
210
|
if (input.write) {
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
// @layer infrastructure
|
|
2
2
|
// @unit nyquist-validation
|
|
3
3
|
// @work-item-id WI-125
|
|
4
|
+
// @work-item-id WI-222
|
|
4
5
|
|
|
5
6
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import type { TestReferenceSourcePort } from '../../application/usecases/generate-requirement-test-matrix-usecase.js';
|
|
8
|
-
import type { TestReferenceSourceDto } from '../../application/dto/generate-matrix-output.js';
|
|
9
|
+
import type { OrphanAcTagDto, TestReferenceSourceDto } from '../../application/dto/generate-matrix-output.js';
|
|
9
10
|
|
|
10
11
|
// StoryId は HXX-XX 形式に加え Phase 2 拡張 Epic の HF\d+-XX 形式も許容する
|
|
11
12
|
// (markdown-requirement-source-adapter の STORY_HEADING / traceability-model の StoryId 正規表現と整合)。
|
|
12
13
|
// 旧 /H\d{2}-\d{2}/ は HF2-01 等を取りこぼし、正しく注釈されたテストが「テストなし」と誤判定されていた。
|
|
13
14
|
const STORY_TAG = /@story(?:-id)?\s+(H(?:F\d+|\d{2})-\d{2})/;
|
|
14
|
-
const
|
|
15
|
+
const STORY_TAG_GLOBAL = /@story(?:-id)?\s+(H(?:F\d+|\d{2})-\d{2})/g;
|
|
16
|
+
// HF2-05: @ac は 1 行に複数 AC を許容する。行末までの全トークンを個別に解釈する。
|
|
17
|
+
// 各トークンは絶対形式 `HXX-YY-N`(story-relative でない)または相対形式 `AC-N`。
|
|
18
|
+
const AC_TAG_LINE = /@ac\s+(.+?)\s*$/;
|
|
19
|
+
const AC_ABSOLUTE_TOKEN = /^(H(?:F\d+|\d{2})-\d{2})-([1-9][0-9]*)$/;
|
|
20
|
+
const AC_RELATIVE_TOKEN = /^AC-([1-9][0-9]*)$/;
|
|
21
|
+
// テストケース開始行の検出(it / test、.each やスペースを許容)。
|
|
22
|
+
const TEST_START = /\b(?:it|test)(?:\.each\([^)]*\))?\s*\(\s*['"`]([^'"`]+)['"`]/;
|
|
23
|
+
|
|
24
|
+
interface PendingAcToken {
|
|
25
|
+
readonly rawTag: string;
|
|
26
|
+
readonly resolvedAcId: string | null;
|
|
27
|
+
readonly reason: OrphanAcTagDto['reason'] | null;
|
|
28
|
+
}
|
|
15
29
|
|
|
16
30
|
async function collectTestFiles(root: string): Promise<readonly string[]> {
|
|
17
31
|
const rootStat = await stat(root).catch(() => null);
|
|
@@ -43,15 +57,103 @@ export class TypeScriptTestReferenceSourceAdapter implements TestReferenceSource
|
|
|
43
57
|
const storyId = storyMatch[1];
|
|
44
58
|
const relativePath = path.relative(process.cwd(), filePath).replaceAll(path.sep, '/');
|
|
45
59
|
const testType = classifyTestType(relativePath);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
// HF2-05: 相対 @ac (AC-N) はファイルに @story がちょうど 1 件のときのみ解決する。
|
|
61
|
+
const storyCount = [...content.matchAll(STORY_TAG_GLOBAL)].length;
|
|
62
|
+
references.push(...this.scanFile({ content, storyId, relativePath, testType, storyCount }));
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze(references);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* HF2-05: ファイル内容を行単位で走査し、直前の it()/test() 以降に現れた @ac を
|
|
69
|
+
* 「最近接の @ac」として次のテストケースへ紐づける(positional scan)。
|
|
70
|
+
* @ac が無いテストは従来どおり acIds 無しの参照を生成する。
|
|
71
|
+
*/
|
|
72
|
+
private scanFile(args: {
|
|
73
|
+
content: string;
|
|
74
|
+
storyId: string;
|
|
75
|
+
relativePath: string;
|
|
76
|
+
testType: 'unit' | 'it' | 'scenario';
|
|
77
|
+
storyCount: number;
|
|
78
|
+
}): TestReferenceSourceDto[] {
|
|
79
|
+
const { content, storyId, relativePath, testType, storyCount } = args;
|
|
80
|
+
const lines = content.split('\n');
|
|
81
|
+
const result: TestReferenceSourceDto[] = [];
|
|
82
|
+
let pending: PendingAcToken[] = [];
|
|
83
|
+
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
const acMatch = line.match(AC_TAG_LINE);
|
|
86
|
+
if (acMatch) {
|
|
87
|
+
for (const token of acMatch[1].trim().split(/\s+/)) {
|
|
88
|
+
if (token.length > 0) pending.push(this.resolveAcToken(token, storyId, storyCount));
|
|
89
|
+
}
|
|
49
90
|
continue;
|
|
50
91
|
}
|
|
51
|
-
|
|
52
|
-
|
|
92
|
+
const testMatch = line.match(TEST_START);
|
|
93
|
+
if (testMatch) {
|
|
94
|
+
result.push(this.buildReference({
|
|
95
|
+
storyId,
|
|
96
|
+
relativePath,
|
|
97
|
+
testType,
|
|
98
|
+
testName: testMatch[1],
|
|
99
|
+
pending,
|
|
100
|
+
}));
|
|
101
|
+
pending = [];
|
|
53
102
|
}
|
|
54
103
|
}
|
|
55
|
-
|
|
104
|
+
|
|
105
|
+
// it()/test() が 1 件も無い注釈済みファイルは、file-level 参照 1 件を維持する(従来挙動)。
|
|
106
|
+
if (result.length === 0) {
|
|
107
|
+
result.push({ storyId, filePath: relativePath, testType });
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private buildReference(args: {
|
|
113
|
+
storyId: string;
|
|
114
|
+
relativePath: string;
|
|
115
|
+
testType: 'unit' | 'it' | 'scenario';
|
|
116
|
+
testName: string;
|
|
117
|
+
pending: readonly PendingAcToken[];
|
|
118
|
+
}): TestReferenceSourceDto {
|
|
119
|
+
const { storyId, relativePath, testType, testName, pending } = args;
|
|
120
|
+
const base: TestReferenceSourceDto = { storyId, filePath: relativePath, testType, testName };
|
|
121
|
+
if (pending.length === 0) return base;
|
|
122
|
+
|
|
123
|
+
const acIds = pending.filter((p) => p.resolvedAcId !== null).map((p) => p.resolvedAcId as string);
|
|
124
|
+
const orphanAcTags: OrphanAcTagDto[] = pending
|
|
125
|
+
.filter((p) => p.resolvedAcId === null && p.reason !== null)
|
|
126
|
+
.map((p) => ({ storyId, filePath: relativePath, testName, rawTag: p.rawTag, reason: p.reason as OrphanAcTagDto['reason'] }));
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
...(acIds.length > 0 ? { acIds: Object.freeze([...new Set(acIds)]) } : {}),
|
|
131
|
+
...(orphanAcTags.length > 0 ? { orphanAcTags: Object.freeze(orphanAcTags) } : {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 1 個の @ac トークンを解釈する。
|
|
137
|
+
* - 絶対形式 `HXX-YY-N`: story が一致すれば `AC-N` に解決、不一致なら ac-not-in-story orphan。
|
|
138
|
+
* - 相対形式 `AC-N`: @story が単一なら `AC-N` に解決、複数なら relative-multi-story orphan。
|
|
139
|
+
*/
|
|
140
|
+
private resolveAcToken(token: string, storyId: string, storyCount: number): PendingAcToken {
|
|
141
|
+
const absolute = token.match(AC_ABSOLUTE_TOKEN);
|
|
142
|
+
if (absolute) {
|
|
143
|
+
const [, tokenStory, acNumber] = absolute;
|
|
144
|
+
if (tokenStory === storyId) {
|
|
145
|
+
return { rawTag: token, resolvedAcId: `AC-${acNumber}`, reason: null };
|
|
146
|
+
}
|
|
147
|
+
return { rawTag: token, resolvedAcId: null, reason: 'ac-not-in-story' };
|
|
148
|
+
}
|
|
149
|
+
const relative = token.match(AC_RELATIVE_TOKEN);
|
|
150
|
+
if (relative) {
|
|
151
|
+
if (storyCount === 1) {
|
|
152
|
+
return { rawTag: token, resolvedAcId: `AC-${relative[1]}`, reason: null };
|
|
153
|
+
}
|
|
154
|
+
return { rawTag: token, resolvedAcId: null, reason: 'relative-multi-story' };
|
|
155
|
+
}
|
|
156
|
+
// 形式不明トークンは advisory orphan として ac-not-in-story 扱い。
|
|
157
|
+
return { rawTag: token, resolvedAcId: null, reason: 'ac-not-in-story' };
|
|
56
158
|
}
|
|
57
159
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit nyquist-validation
|
|
4
|
+
* @work-item-id WI-222
|
|
4
5
|
*
|
|
5
6
|
* requirement-test-matrix.schema.json ローダー(シングルトンキャッシュ付き)
|
|
7
|
+
*
|
|
8
|
+
* HF2-05 (schema 1.1): testReferences に optional な `binding` enum("ac"|"file")を追加。
|
|
9
|
+
* 1.1 は追加フィールドが optional かつ additionalProperties:false を満たすため、
|
|
10
|
+
* `binding` を持たない 1.0 マトリクスも引き続き検証を通過する(後方互換)。
|
|
6
11
|
*/
|
|
7
12
|
import { readFile } from 'node:fs/promises';
|
|
8
13
|
import { resolve, join } from 'node:path';
|
|
@@ -23,6 +23,8 @@ import type { DeadCodeDetectionService } from '../../domain/services/l4/dead-cod
|
|
|
23
23
|
import type { ArchitectureSemanticAnalysisService } from '../../domain/services/l4/architecture-semantic-analysis-service.js';
|
|
24
24
|
import { SkillCatalogDriftService } from '../../domain/services/l4/skill-catalog-drift-service.js';
|
|
25
25
|
import type { SkillCatalogDriftPort } from '../../domain/ports/skill-catalog-drift-port.js';
|
|
26
|
+
import { AcLevelTraceabilityService } from '../../domain/services/l4/ac-level-traceability-service.js';
|
|
27
|
+
import type { AcLevelTraceabilityPort } from '../../domain/ports/ac-level-traceability-port.js';
|
|
26
28
|
|
|
27
29
|
interface ScheduledHarnessErrorContract {
|
|
28
30
|
readonly severity: string;
|
|
@@ -73,6 +75,7 @@ export interface RunL4ValidatorsUseCaseDeps {
|
|
|
73
75
|
deadCodeDetectionService?: DeadCodeDetectionService;
|
|
74
76
|
architectureSemanticAnalysisService?: ArchitectureSemanticAnalysisService;
|
|
75
77
|
skillCatalogDriftPort?: SkillCatalogDriftPort;
|
|
78
|
+
acLevelTraceabilityPort?: AcLevelTraceabilityPort;
|
|
76
79
|
pathRoots?: {
|
|
77
80
|
readonly inceptionRoot: string;
|
|
78
81
|
readonly designRoot: string;
|
|
@@ -92,6 +95,8 @@ export class RunL4ValidatorsUseCase {
|
|
|
92
95
|
private readonly architectureSemanticAnalysisService?: ArchitectureSemanticAnalysisService;
|
|
93
96
|
private readonly skillCatalogDriftPort?: SkillCatalogDriftPort;
|
|
94
97
|
private readonly skillCatalogDriftService = new SkillCatalogDriftService();
|
|
98
|
+
private readonly acLevelTraceabilityPort?: AcLevelTraceabilityPort;
|
|
99
|
+
private readonly acLevelTraceabilityService = new AcLevelTraceabilityService();
|
|
95
100
|
private readonly pathRoots: { readonly inceptionRoot: string; readonly designRoot: string };
|
|
96
101
|
private readonly checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
|
|
97
102
|
private readonly validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
|
|
@@ -107,6 +112,7 @@ export class RunL4ValidatorsUseCase {
|
|
|
107
112
|
this.deadCodeDetectionService = deps.deadCodeDetectionService;
|
|
108
113
|
this.architectureSemanticAnalysisService = deps.architectureSemanticAnalysisService;
|
|
109
114
|
this.skillCatalogDriftPort = deps.skillCatalogDriftPort;
|
|
115
|
+
this.acLevelTraceabilityPort = deps.acLevelTraceabilityPort;
|
|
110
116
|
this.pathRoots = deps.pathRoots ?? {
|
|
111
117
|
inceptionRoot: 'docs/inception',
|
|
112
118
|
designRoot: 'docs/product/construction',
|
|
@@ -263,6 +269,23 @@ export class RunL4ValidatorsUseCase {
|
|
|
263
269
|
}
|
|
264
270
|
}
|
|
265
271
|
|
|
272
|
+
if (this.acLevelTraceabilityPort) {
|
|
273
|
+
const l4007Result = overrideMap.get('L4-007');
|
|
274
|
+
// default-OFF: L4-007 が enabled validator set に含まれず skip される場合は上書きしない。
|
|
275
|
+
if (l4007Result && !l4007Result.skipped) {
|
|
276
|
+
const snapshot = await this.acLevelTraceabilityPort.collect();
|
|
277
|
+
const report = this.acLevelTraceabilityService.check(snapshot);
|
|
278
|
+
// advisory-only: finding があっても severity は必ず warning。overall gate は
|
|
279
|
+
// failOnWarning=false(既定)で PASS のまま(ADR-019 §5)。finding が無ければ PASS。
|
|
280
|
+
overrideMap.set(
|
|
281
|
+
'L4-007',
|
|
282
|
+
report.hasFindings()
|
|
283
|
+
? ValidationResult.fail(ValidatorId.create('L4-007'), report.toHarnessErrors(), 0)
|
|
284
|
+
: ValidationResult.pass(ValidatorId.create('L4-007'), 0),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
266
289
|
const finalResults = definitions.map(
|
|
267
290
|
(definition) => overrideMap.get(definition.validatorId.value) ?? ValidationResult.skip(definition.validatorId),
|
|
268
291
|
);
|
|
@@ -39,6 +39,7 @@ import { AdrFoundationReferenceAdapter } from './infrastructure/adapters/adr-fou
|
|
|
39
39
|
import { ImportGraphSourceAnalysisAdapter } from './infrastructure/adapters/import-graph-source-analysis-adapter.js';
|
|
40
40
|
import { FileSystemArchitectureSemanticSourceAdapter } from './infrastructure/adapters/file-system-architecture-semantic-source-adapter.js';
|
|
41
41
|
import { FileSystemSkillCatalogDriftAdapter } from './infrastructure/adapters/file-system-skill-catalog-drift-adapter.js';
|
|
42
|
+
import { NyquistAcLevelTraceabilityAdapter } from './infrastructure/adapters/nyquist-ac-level-traceability-adapter.js';
|
|
42
43
|
import { FileSystemWorkItemReflectionAdapter } from './infrastructure/adapters/file-system-work-item-reflection-adapter.js';
|
|
43
44
|
import { DriftDetectionService } from './domain/services/l4/drift-detection-service.js';
|
|
44
45
|
import { ConsistencyCheckService } from './domain/services/l4/consistency-check-service.js';
|
|
@@ -117,6 +118,9 @@ export function buildDefaultRegistry(): ValidatorRegistry {
|
|
|
117
118
|
createDef('L4-004', 'L4', 'always'),
|
|
118
119
|
createDef('L4-005', 'L4', 'always'),
|
|
119
120
|
createDef('L4-006', 'L4', 'always', 'SkillCatalogDriftPort'),
|
|
121
|
+
// WI-222 / HF2-05: L4-007 (ac-level-traceability) は registry には登録するが
|
|
122
|
+
// default-OFF(DEFAULT_CONFIG.layers.L4.validators に含めない)。advisory-only。
|
|
123
|
+
createDef('L4-007', 'L4', 'always', 'AcLevelTraceabilityPort'),
|
|
120
124
|
];
|
|
121
125
|
|
|
122
126
|
return new ValidatorRegistry(definitions);
|
|
@@ -206,6 +210,11 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
206
210
|
const sourceAnalysisPort = new ImportGraphSourceAnalysisAdapter();
|
|
207
211
|
const architectureSemanticSourcePort = new FileSystemArchitectureSemanticSourceAdapter();
|
|
208
212
|
const skillCatalogDriftPort = new FileSystemSkillCatalogDriftAdapter(cwd);
|
|
213
|
+
// WI-222 / HF2-05: L4-007 (advisory) 用の AC 単位トレーサビリティ port。
|
|
214
|
+
// matrix path は L3-004 と同じ config キーを参照する。
|
|
215
|
+
const acLevelTraceabilityPort = new NyquistAcLevelTraceabilityAdapter({
|
|
216
|
+
matrixFilePath: configData.layers?.L3?.requirementMatrixPath ?? '.harness/requirement-test-matrix.json',
|
|
217
|
+
});
|
|
209
218
|
const workItemReflectionPort = new FileSystemWorkItemReflectionAdapter(cwd);
|
|
210
219
|
|
|
211
220
|
const driftDetectionService = new DriftDetectionService({
|
|
@@ -236,6 +245,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
236
245
|
deadCodeDetectionService,
|
|
237
246
|
architectureSemanticAnalysisService,
|
|
238
247
|
skillCatalogDriftPort,
|
|
248
|
+
acLevelTraceabilityPort,
|
|
239
249
|
pathRoots: {
|
|
240
250
|
inceptionRoot: inceptionDocsRoot,
|
|
241
251
|
designRoot: designDocsRoot,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-222
|
|
5
|
+
*
|
|
6
|
+
* HF2-05 / L4-007: nyquist-validation のマトリクス生成レポートを AC 単位トレーサビリティの
|
|
7
|
+
* snapshot へ橋渡しする port。実装(infrastructure adapter)が nyquist の generate-matrix
|
|
8
|
+
* usecase を実行し、acLevelCoverage / fileFallbackOnlyAcs / orphanAcTags を収集する。
|
|
9
|
+
*/
|
|
10
|
+
import type { AcLevelTraceabilitySnapshot } from '../services/l4/ac-level-traceability-service.js';
|
|
11
|
+
|
|
12
|
+
export interface AcLevelTraceabilityPort {
|
|
13
|
+
collect(): Promise<AcLevelTraceabilitySnapshot>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-222
|
|
5
|
+
*
|
|
6
|
+
* HF2-05 / L4-007: AC 単位トレーサビリティ advisory サービス。
|
|
7
|
+
*
|
|
8
|
+
* このサービスは fileFallbackOnly な AC(story-level では linked だが、個別 AC を検証する
|
|
9
|
+
* @ac 注釈付きテストが 1 件も無い AC)と orphanAcTags(解決に失敗した @ac)を
|
|
10
|
+
* warning severity の finding へ変換する。**error は一切出さない**(ADR-019 §5: advisory tier は
|
|
11
|
+
* non-blocking でなければならない)。fileFallbackOnly が 0 かつ orphanAcTags が空なら PASS。
|
|
12
|
+
*
|
|
13
|
+
* L3-004 の二値 pass/fail 判定には一切関与しない(verdict 不変)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface AcLevelCoverageSummary {
|
|
17
|
+
readonly total: number;
|
|
18
|
+
readonly acBound: number;
|
|
19
|
+
readonly fileFallbackOnly: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FileFallbackOnlyAc {
|
|
23
|
+
readonly storyId: string;
|
|
24
|
+
readonly acId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface OrphanAcTag {
|
|
28
|
+
readonly storyId: string;
|
|
29
|
+
readonly filePath: string;
|
|
30
|
+
readonly testName?: string;
|
|
31
|
+
readonly rawTag: string;
|
|
32
|
+
readonly reason: 'ac-not-in-story' | 'relative-multi-story';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AcLevelTraceabilitySnapshot {
|
|
36
|
+
readonly acLevelCoverage: AcLevelCoverageSummary;
|
|
37
|
+
readonly fileFallbackOnlyAcs: readonly FileFallbackOnlyAc[];
|
|
38
|
+
readonly orphanAcTags: readonly OrphanAcTag[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AcLevelTraceabilityFinding {
|
|
42
|
+
readonly kind: 'file-fallback-only-ac' | 'orphan-ac-tag';
|
|
43
|
+
readonly message: string;
|
|
44
|
+
readonly suggestion: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class AcLevelTraceabilityReport {
|
|
48
|
+
readonly findings: readonly AcLevelTraceabilityFinding[];
|
|
49
|
+
|
|
50
|
+
constructor(findings: readonly AcLevelTraceabilityFinding[]) {
|
|
51
|
+
this.findings = Object.freeze([...findings]);
|
|
52
|
+
Object.freeze(this);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
hasFindings(): boolean {
|
|
56
|
+
return this.findings.length > 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
toHarnessErrors(): readonly {
|
|
60
|
+
readonly code: { readonly value: string; toString(): string };
|
|
61
|
+
readonly severity: { readonly value: string; toString(): string };
|
|
62
|
+
readonly message: string;
|
|
63
|
+
readonly suggestion: string;
|
|
64
|
+
readonly kind: string;
|
|
65
|
+
}[] {
|
|
66
|
+
return this.findings.map((finding) => ({
|
|
67
|
+
code: { value: 'L4-007', toString: () => 'L4-007' },
|
|
68
|
+
severity: { value: 'warning', toString: () => 'warning' },
|
|
69
|
+
message: finding.message,
|
|
70
|
+
suggestion: finding.suggestion,
|
|
71
|
+
kind: finding.kind,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class AcLevelTraceabilityService {
|
|
77
|
+
check(snapshot: AcLevelTraceabilitySnapshot): AcLevelTraceabilityReport {
|
|
78
|
+
const findings: AcLevelTraceabilityFinding[] = [];
|
|
79
|
+
|
|
80
|
+
for (const ac of snapshot.fileFallbackOnlyAcs) {
|
|
81
|
+
findings.push({
|
|
82
|
+
kind: 'file-fallback-only-ac',
|
|
83
|
+
message: `${ac.storyId} ${ac.acId} is linked only via file-fallback (no @ac-bound test asserts it individually).`,
|
|
84
|
+
suggestion: `Add an "// @ac ${ac.storyId}-${ac.acId.replace(/^AC-/, '')}" annotation to the test case that specifically asserts ${ac.acId}, or accept this as a known file-level gap.`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const orphan of snapshot.orphanAcTags) {
|
|
89
|
+
const location = orphan.testName ? `${orphan.filePath} ("${orphan.testName}")` : orphan.filePath;
|
|
90
|
+
const reasonText = orphan.reason === 'relative-multi-story'
|
|
91
|
+
? 'relative @ac cannot be resolved because the file declares multiple @story tags'
|
|
92
|
+
: 'the referenced AC does not belong to the file\'s story';
|
|
93
|
+
findings.push({
|
|
94
|
+
kind: 'orphan-ac-tag',
|
|
95
|
+
message: `${location}: @ac "${orphan.rawTag}" is unresolved — ${reasonText}.`,
|
|
96
|
+
suggestion: 'Use an absolute @ac (HXX-YY-N) that matches the file\'s @story, or split multi-story test files so relative AC-N resolves unambiguously.',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return new AcLevelTraceabilityReport(findings);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* WI-140 で L2-014 を追加
|
|
10
10
|
* WI-132/WI-133/WI-136/WI-137/WI-138 で L2-015 を追加
|
|
11
11
|
* WI-156 で L4-006 を追加
|
|
12
|
+
* WI-222 (HF2-05) で L4-007(ac-level-traceability, default-OFF advisory)を追加
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
export class InvalidValidatorIdError extends Error {
|
|
@@ -42,6 +43,7 @@ const VALIDATOR_NAME_MAP: Record<string, string> = {
|
|
|
42
43
|
'L4-004': 'doc-freshness',
|
|
43
44
|
'L4-005': 'pointer-validation',
|
|
44
45
|
'L4-006': 'skill-catalog-drift',
|
|
46
|
+
'L4-007': 'ac-level-traceability',
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
/** バリデータ名 -> バリデータID の逆引きマップ */
|
|
@@ -53,6 +55,7 @@ const NAME_TO_ID_MAP: Record<string, string> = {
|
|
|
53
55
|
'doc-freshness-checker': 'L4-004',
|
|
54
56
|
'pointer-validator': 'L4-005',
|
|
55
57
|
'skill-catalog-drift': 'L4-006',
|
|
58
|
+
'ac-level-traceability': 'L4-007',
|
|
56
59
|
};
|
|
57
60
|
|
|
58
61
|
/** 有効なValidatorID集合 */
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer infrastructure
|
|
3
|
+
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-222
|
|
5
|
+
*
|
|
6
|
+
* HF2-05 / L4-007: nyquist-validation の generate-matrix usecase を実行し、
|
|
7
|
+
* AC 単位トレーサビリティの snapshot(acLevelCoverage / fileFallbackOnlyAcs / orphanAcTags)を
|
|
8
|
+
* 収集して AcLevelTraceabilityPort として提供する adapter。
|
|
9
|
+
*
|
|
10
|
+
* advisory 用途のため、収集に失敗しても throw せず「所見なし」snapshot を返す(fail-open)。
|
|
11
|
+
* これは L3-004(fail-closed な AC 網羅ゲート)とは独立した advisory tier であり、
|
|
12
|
+
* 収集失敗が CI を落とすことは意図しない(ADR-019 §5: advisory は non-blocking)。
|
|
13
|
+
*/
|
|
14
|
+
import type { AcLevelTraceabilityPort } from '../../domain/ports/ac-level-traceability-port.js';
|
|
15
|
+
import type {
|
|
16
|
+
AcLevelTraceabilitySnapshot,
|
|
17
|
+
FileFallbackOnlyAc,
|
|
18
|
+
OrphanAcTag,
|
|
19
|
+
} from '../../domain/services/l4/ac-level-traceability-service.js';
|
|
20
|
+
|
|
21
|
+
const EMPTY_SNAPSHOT: AcLevelTraceabilitySnapshot = Object.freeze({
|
|
22
|
+
acLevelCoverage: { total: 0, acBound: 0, fileFallbackOnly: 0 },
|
|
23
|
+
fileFallbackOnlyAcs: Object.freeze([]),
|
|
24
|
+
orphanAcTags: Object.freeze([]),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export interface NyquistAcLevelTraceabilityAdapterOptions {
|
|
28
|
+
readonly requirementsPath?: string;
|
|
29
|
+
readonly testRoot?: string;
|
|
30
|
+
readonly matrixFilePath?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class NyquistAcLevelTraceabilityAdapter implements AcLevelTraceabilityPort {
|
|
34
|
+
private readonly requirementsPath: string;
|
|
35
|
+
private readonly testRoot: string;
|
|
36
|
+
private readonly matrixFilePath: string;
|
|
37
|
+
|
|
38
|
+
constructor(options: NyquistAcLevelTraceabilityAdapterOptions = {}) {
|
|
39
|
+
this.requirementsPath = options.requirementsPath ?? 'docs/product/user_stories.md';
|
|
40
|
+
this.testRoot = options.testRoot ?? 'scripts/harness/__tests__';
|
|
41
|
+
this.matrixFilePath = options.matrixFilePath ?? '.harness/requirement-test-matrix.json';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async collect(): Promise<AcLevelTraceabilitySnapshot> {
|
|
45
|
+
try {
|
|
46
|
+
const storyIds = await this.loadValidStoryIds();
|
|
47
|
+
const { createNyquistValidationModule } = await import('../../../nyquist-validation/composition-root.js');
|
|
48
|
+
const mod = createNyquistValidationModule({ getStoryIds: async () => storyIds });
|
|
49
|
+
const output = await mod.generateMatrixUseCase.execute({
|
|
50
|
+
requirementsPath: this.requirementsPath,
|
|
51
|
+
testRoot: this.testRoot,
|
|
52
|
+
matrixFilePath: this.matrixFilePath,
|
|
53
|
+
write: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const fileFallbackOnlyAcs: FileFallbackOnlyAc[] = [];
|
|
57
|
+
for (const story of output.matrix.stories) {
|
|
58
|
+
for (const mapping of story.storyMappings) {
|
|
59
|
+
if (mapping.testReferences.length === 0) continue;
|
|
60
|
+
const hasAcBound = mapping.testReferences.some((ref) => ref.binding === 'ac');
|
|
61
|
+
if (!hasAcBound) {
|
|
62
|
+
fileFallbackOnlyAcs.push({ storyId: story.storyId, acId: mapping.acId });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const orphanAcTags: OrphanAcTag[] = output.report.orphanAcTags.map((tag) => ({
|
|
68
|
+
storyId: tag.storyId,
|
|
69
|
+
filePath: tag.filePath,
|
|
70
|
+
testName: tag.testName,
|
|
71
|
+
rawTag: tag.rawTag,
|
|
72
|
+
reason: tag.reason,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
acLevelCoverage: {
|
|
77
|
+
total: output.report.acLevelCoverage.total,
|
|
78
|
+
acBound: output.report.acLevelCoverage.acBound,
|
|
79
|
+
fileFallbackOnly: output.report.acLevelCoverage.fileFallbackOnly,
|
|
80
|
+
},
|
|
81
|
+
fileFallbackOnlyAcs: Object.freeze(fileFallbackOnlyAcs),
|
|
82
|
+
orphanAcTags: Object.freeze(orphanAcTags),
|
|
83
|
+
};
|
|
84
|
+
} catch {
|
|
85
|
+
return EMPTY_SNAPSHOT;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async loadValidStoryIds(): Promise<readonly string[]> {
|
|
90
|
+
try {
|
|
91
|
+
const { createConfigFoundationModule } = await import('../../../config-foundation/composition-root.js');
|
|
92
|
+
const configModule = createConfigFoundationModule();
|
|
93
|
+
const resolvedConfig = await configModule.usecases.loadResolvedConfigUseCase.execute();
|
|
94
|
+
const { createTraceabilityModelModule } = await import('../../../traceability-model/composition-root.js');
|
|
95
|
+
const traceModule = createTraceabilityModelModule(process.cwd(), {
|
|
96
|
+
pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs },
|
|
97
|
+
});
|
|
98
|
+
const storyIds = await traceModule.storyCatalog.getAllStoryIds();
|
|
99
|
+
return storyIds.map((s) => s.value);
|
|
100
|
+
} catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|