phasegate 0.165.0 → 0.171.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/docs/ADR/021-severity-contract.md +46 -0
  3. package/docs/contracts/requirement-test-matrix.schema.json +5 -0
  4. package/package.json +1 -1
  5. package/scripts/harness/attestation/application/dto/attestation-document.ts +2 -0
  6. package/scripts/harness/attestation/application/dto/verify-attestation-output.ts +2 -0
  7. package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +13 -0
  8. package/scripts/harness/attestation/application/ports/ac-bound-allowlist-port.ts +12 -0
  9. package/scripts/harness/attestation/application/ports/matrix-source-port.ts +16 -0
  10. package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +39 -0
  11. package/scripts/harness/attestation/application/usecases/verify-attestation-usecase.ts +60 -1
  12. package/scripts/harness/attestation/composition-root.ts +14 -0
  13. package/scripts/harness/attestation/domain/entities/attestation-record.ts +13 -0
  14. package/scripts/harness/attestation/domain/services/ac-bound-scope-service.ts +80 -0
  15. package/scripts/harness/attestation/infrastructure/adapters/config-ac-bound-allowlist-adapter.ts +23 -0
  16. package/scripts/harness/attestation/infrastructure/adapters/file-system-matrix-source-adapter.ts +25 -0
  17. package/scripts/harness/attestation/presentation/handlers/verify-attestation-handler.ts +1 -0
  18. package/scripts/harness/ci-governance/infrastructure/adapters/validator-id-registry-adapter.ts +8 -0
  19. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +3 -0
  20. package/scripts/harness/config-foundation/domain/harness-config.ts +1 -0
  21. package/scripts/harness/config-foundation/domain/value-objects/l3-config.ts +4 -0
  22. package/scripts/harness/config-foundation/domain/value-objects/layers-config.ts +1 -1
  23. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +7 -0
  24. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +7 -0
  25. package/scripts/harness/harness-error/domain/errors/severity-downgrade-violation-error.ts +1 -1
  26. package/scripts/harness/main.ts +2 -2
  27. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +42 -0
  28. package/scripts/harness/nyquist-validation/application/mappers/coverage-result-mapper.ts +13 -15
  29. package/scripts/harness/nyquist-validation/application/mappers/impact-analysis-result-mapper.ts +12 -12
  30. package/scripts/harness/nyquist-validation/application/usecases/analyze-impact-usecase.ts +11 -11
  31. package/scripts/harness/nyquist-validation/application/usecases/calculate-coverage-usecase.ts +11 -11
  32. package/scripts/harness/nyquist-validation/application/usecases/check-ac-coverage-gate-usecase.ts +9 -9
  33. package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +62 -7
  34. package/scripts/harness/nyquist-validation/domain/aggregates/requirement-test-matrix.ts +1 -1
  35. package/scripts/harness/nyquist-validation/domain/entities/story-mapping.ts +1 -1
  36. package/scripts/harness/nyquist-validation/domain/services/ac-coverage-gate-policy.ts +1 -1
  37. package/scripts/harness/nyquist-validation/domain/services/coverage-calculation-service.ts +1 -1
  38. package/scripts/harness/nyquist-validation/domain/services/impact-analysis-service.ts +2 -2
  39. package/scripts/harness/nyquist-validation/domain/services/matrix-validation-service.ts +5 -5
  40. package/scripts/harness/nyquist-validation/domain/value-objects/impact-analysis-result.ts +1 -1
  41. package/scripts/harness/nyquist-validation/infrastructure/adapters/ajv-json-schema-validator-adapter.ts +3 -3
  42. package/scripts/harness/nyquist-validation/infrastructure/adapters/config-foundation-coverage-threshold-adapter.ts +3 -3
  43. package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +110 -8
  44. package/scripts/harness/nyquist-validation/infrastructure/schema/matrix-schema-loader.ts +5 -0
  45. package/scripts/harness/validator-system/application/dto/run-l3-validators-input.ts +2 -0
  46. package/scripts/harness/validator-system/application/use-cases/run-l1-validators-usecase.ts +1 -1
  47. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +3 -3
  48. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +33 -3
  49. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +26 -3
  50. package/scripts/harness/validator-system/application/use-cases/run-quick-mode-usecase.ts +3 -3
  51. package/scripts/harness/validator-system/composition-root.ts +22 -0
  52. package/scripts/harness/validator-system/domain/ports/ac-bound-coverage-policy-port.ts +22 -0
  53. package/scripts/harness/validator-system/domain/ports/ac-level-traceability-port.ts +14 -0
  54. package/scripts/harness/validator-system/domain/services/l4/ac-level-traceability-service.ts +102 -0
  55. package/scripts/harness/validator-system/domain/services/validator-execution-service.ts +1 -1
  56. package/scripts/harness/validator-system/domain/services/validator-registry.ts +2 -2
  57. package/scripts/harness/validator-system/domain/value-objects/layer-config.ts +1 -1
  58. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +1 -1
  59. package/scripts/harness/validator-system/domain/value-objects/validator-definition.ts +2 -2
  60. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +5 -0
  61. package/scripts/harness/validator-system/infrastructure/adapters/json-coverage-report-adapter.ts +1 -1
  62. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-bound-coverage-policy-adapter.ts +118 -0
  63. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-level-traceability-adapter.ts +104 -0
  64. package/scripts/harness/validator-system/presentation/handlers/report-validation-results-handler.ts +4 -4
@@ -0,0 +1,25 @@
1
+ // @unit attestation
2
+ // @layer infrastructure
3
+
4
+ import { readFile } from "node:fs/promises";
5
+ import { isAbsolute, resolve } from "node:path";
6
+ import type { MatrixSourcePort } from "../../application/ports/matrix-source-port.js";
7
+
8
+ /**
9
+ * MatrixSourcePort の node:fs 実装(H16-03 / WI-227)。
10
+ * requirement-test-matrix.json を読み込み parse して返す。
11
+ * 不在・parse 不能は throw する(呼び出し側 usecase が fail-closed / [] に変換する)。
12
+ */
13
+ export class FileSystemMatrixSourceAdapter implements MatrixSourcePort {
14
+ constructor(
15
+ private readonly baseDir: string,
16
+ private readonly defaultMatrixPath = ".harness/requirement-test-matrix.json",
17
+ ) {}
18
+
19
+ async load(matrixFilePath?: string): Promise<unknown> {
20
+ const rel = matrixFilePath && matrixFilePath.length > 0 ? matrixFilePath : this.defaultMatrixPath;
21
+ const absPath = isAbsolute(rel) ? rel : resolve(this.baseDir, rel);
22
+ const raw = await readFile(absPath, "utf8");
23
+ return JSON.parse(raw);
24
+ }
25
+ }
@@ -55,6 +55,7 @@ function renderHuman(output: VerifyAttestationOutput, exitCode: 0 | 1 | 2): stri
55
55
  lines.push(`attestationDigest: ${mark(c.attestationDigest)}`);
56
56
  lines.push(`inputHashes : ${mark(c.inputHashes)}`);
57
57
  lines.push(`granularity : ${mark(c.granularity)}`);
58
+ lines.push(`acBoundScope : ${mark(c.acBoundScope)}`);
58
59
  if (output.mismatches.length > 0) {
59
60
  lines.push("");
60
61
  lines.push("Mismatches:");
@@ -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;
@@ -15,6 +15,7 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
15
15
  performance: 'L3-002',
16
16
  coverage: 'L3-003',
17
17
  nyquist: 'L3-004',
18
+ 'ac-bound-coverage': 'L3-005',
18
19
  }, /^L3-\d{3}$/);
19
20
  const l4Validators = normalizeValidators(resolvedConfig.layers.L4.validators, {
20
21
  'drift-detect': 'L4-001',
@@ -49,6 +50,8 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
49
50
  validators: l3Validators.length > 0 ? l3Validators : ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
50
51
  coverageThreshold: resolvedConfig.layers.L3.coverageThreshold,
51
52
  requirementMatrixPath: resolvedConfig.layers.L3.requirementMatrixPath,
53
+ // WI-227 / H16-03: L3-005 のスコープ(additive-safe。未設定は [])
54
+ acBoundStories: resolvedConfig.layers.L3.acBoundStories ?? [],
52
55
  },
53
56
  L4: {
54
57
  enabled: resolvedConfig.layers.L4.enabled,
@@ -83,6 +83,7 @@ export interface HarnessConfigResolvedDocument {
83
83
  validators: string[];
84
84
  coverageThreshold: number;
85
85
  requirementMatrixPath?: string;
86
+ acBoundStories?: readonly string[];
86
87
  };
87
88
  L4: {
88
89
  enabled: boolean;
@@ -11,6 +11,7 @@ interface L3ConfigProps {
11
11
  readonly validators: string[];
12
12
  readonly coverageThreshold: number;
13
13
  readonly requirementMatrixPath?: string;
14
+ readonly acBoundStories?: readonly string[];
14
15
  }
15
16
 
16
17
  export class L3Config {
@@ -18,6 +19,7 @@ export class L3Config {
18
19
  readonly validators: readonly string[];
19
20
  readonly coverageThreshold: number;
20
21
  readonly requirementMatrixPath?: string;
22
+ readonly acBoundStories?: readonly string[];
21
23
 
22
24
  constructor(props: L3ConfigProps) {
23
25
  if (props.coverageThreshold < 0) {
@@ -40,6 +42,7 @@ export class L3Config {
40
42
  this.validators = Object.freeze([...props.validators]);
41
43
  this.coverageThreshold = props.coverageThreshold;
42
44
  this.requirementMatrixPath = props.requirementMatrixPath;
45
+ this.acBoundStories = props.acBoundStories ? Object.freeze([...props.acBoundStories]) : undefined;
43
46
  Object.freeze(this);
44
47
  }
45
48
 
@@ -48,6 +51,7 @@ export class L3Config {
48
51
  validators: string[];
49
52
  coverageThreshold: number;
50
53
  requirementMatrixPath?: string;
54
+ acBoundStories?: readonly string[];
51
55
  }): L3Config {
52
56
  return new L3Config(raw);
53
57
  }
@@ -13,7 +13,7 @@ import { L4Config } from './l4-config.js';
13
13
  interface ResolvedLayersDocument {
14
14
  L1: { enabled: boolean; rules: Record<string, string> };
15
15
  L2: { enabled: boolean; validators: string[] };
16
- L3: { enabled: boolean; validators: string[]; coverageThreshold: number; requirementMatrixPath?: string };
16
+ L3: { enabled: boolean; validators: string[]; coverageThreshold: number; requirementMatrixPath?: string; acBoundStories?: readonly string[] };
17
17
  L4: { enabled: boolean; validators: string[]; schedule: string };
18
18
  }
19
19
 
@@ -130,6 +130,13 @@
130
130
  "requirementMatrixPath": {
131
131
  "type": "string",
132
132
  "minLength": 1
133
+ },
134
+ "acBoundStories": {
135
+ "type": "array",
136
+ "items": {
137
+ "type": "string"
138
+ },
139
+ "uniqueItems": true
133
140
  }
134
141
  }
135
142
  },
@@ -140,6 +140,13 @@
140
140
  "requirementMatrixPath": {
141
141
  "type": "string",
142
142
  "minLength": 1
143
+ },
144
+ "acBoundStories": {
145
+ "type": "array",
146
+ "items": {
147
+ "type": "string"
148
+ },
149
+ "uniqueItems": true
143
150
  }
144
151
  }
145
152
  },
@@ -6,7 +6,7 @@ import { HarnessErrorDomainError } from './harness-error-domain-error.js';
6
6
  export class SeverityDowngradeViolationError extends HarnessErrorDomainError {
7
7
  constructor(requested: string, defaultSeverity: string) {
8
8
  super(
9
- `severity の格下げは禁止されています: default="${defaultSeverity}" に対して "${requested}" が要求されました。`
9
+ `severity の格下げは禁止されています: default="${defaultSeverity}" に対して "${requested}" が要求されました。根拠: ADR-021`
10
10
  );
11
11
  this.name = 'SeverityDowngradeViolationError';
12
12
  }
@@ -2150,8 +2150,8 @@ async function main(): Promise<void> {
2150
2150
  force: hasFlag(args, "--force"),
2151
2151
  includeClaude,
2152
2152
  includeCodex,
2153
- includeHusky: personal ? false : true,
2154
- includeCi: personal ? false : true,
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 {
@@ -7,19 +7,17 @@
7
7
  import type { CoverageResult } from '../../domain/value-objects/coverage-result.js';
8
8
  import type { CalculateCoverageOutput } from '../dto/calculate-coverage-output.js';
9
9
 
10
- export class CoverageResultMapper {
11
- static toOutput(
12
- result: CoverageResult,
13
- threshold: { active: number } | null
14
- ): CalculateCoverageOutput {
15
- return {
16
- coveredAcCount: result.coveredAcCount,
17
- totalAcCount: result.totalAcCount,
18
- ratePercent: result.toPercentage(),
19
- uncoveredAcIds: result.uncoveredAcIds,
20
- threshold: threshold?.active ?? null,
21
- meetsThreshold:
22
- threshold !== null ? result.meetsThreshold(threshold.active) : null,
23
- };
24
- }
10
+ export function toCalculateCoverageOutput(
11
+ result: CoverageResult,
12
+ threshold: { active: number } | null
13
+ ): CalculateCoverageOutput {
14
+ return {
15
+ coveredAcCount: result.coveredAcCount,
16
+ totalAcCount: result.totalAcCount,
17
+ ratePercent: result.toPercentage(),
18
+ uncoveredAcIds: result.uncoveredAcIds,
19
+ threshold: threshold?.active ?? null,
20
+ meetsThreshold:
21
+ threshold !== null ? result.meetsThreshold(threshold.active) : null,
22
+ };
25
23
  }
@@ -7,16 +7,16 @@
7
7
  import type { ImpactAnalysisResult } from '../../domain/value-objects/impact-analysis-result.js';
8
8
  import type { AnalyzeImpactOutput } from '../dto/analyze-impact-output.js';
9
9
 
10
- export class ImpactAnalysisResultMapper {
11
- static toOutput(result: ImpactAnalysisResult): AnalyzeImpactOutput {
12
- return {
13
- storyId: result.storyId,
14
- directTests: result.directTests.map((ref) => ({
15
- filePath: ref.filePath,
16
- testType: ref.testType,
17
- })),
18
- directMappingOnly: true,
19
- found: !result.isEmpty(),
20
- };
21
- }
10
+ export function toAnalyzeImpactOutput(
11
+ result: ImpactAnalysisResult
12
+ ): AnalyzeImpactOutput {
13
+ return {
14
+ storyId: result.storyId,
15
+ directTests: result.directTests.map((ref) => ({
16
+ filePath: ref.filePath,
17
+ testType: ref.testType,
18
+ })),
19
+ directMappingOnly: true,
20
+ found: !result.isEmpty(),
21
+ };
22
22
  }
@@ -12,7 +12,7 @@ import { RequirementTestMatrix } from '../../domain/aggregates/requirement-test-
12
12
  import { StoryMapping } from '../../domain/entities/story-mapping.js';
13
13
  import { AcMapping } from '../../domain/value-objects/ac-mapping.js';
14
14
  import { TestReference } from '../../domain/value-objects/test-reference.js';
15
- import { ImpactAnalysisResultMapper } from '../mappers/impact-analysis-result-mapper.js';
15
+ import { toAnalyzeImpactOutput } from '../mappers/impact-analysis-result-mapper.js';
16
16
  import type { AnalyzeImpactInput } from '../dto/analyze-impact-input.js';
17
17
  import type { AnalyzeImpactOutput } from '../dto/analyze-impact-output.js';
18
18
 
@@ -25,29 +25,29 @@ export interface AnalyzeImpactUseCaseDeps {
25
25
 
26
26
  function buildMatrix(data: unknown): RequirementTestMatrix {
27
27
  const obj = data as Record<string, unknown>;
28
- const rawStories = Array.isArray(obj['stories']) ? obj['stories'] : (Array.isArray(obj['storyMappings']) ? obj['storyMappings'] : []);
28
+ const rawStories = Array.isArray(obj.stories) ? obj.stories : (Array.isArray(obj.storyMappings) ? obj.storyMappings : []);
29
29
 
30
30
  const storyMappings = (rawStories as unknown[]).map((s) => {
31
31
  const story = s as Record<string, unknown>;
32
- const rawAcMappings = Array.isArray(story['storyMappings'])
33
- ? story['storyMappings']
34
- : (Array.isArray(story['acMappings']) ? story['acMappings'] : []);
32
+ const rawAcMappings = Array.isArray(story.storyMappings)
33
+ ? story.storyMappings
34
+ : (Array.isArray(story.acMappings) ? story.acMappings : []);
35
35
 
36
36
  const acMappings = (rawAcMappings as unknown[]).map((a) => {
37
37
  const acm = a as Record<string, unknown>;
38
- const rawRefs = Array.isArray(acm['testReferences']) ? acm['testReferences'] : [];
38
+ const rawRefs = Array.isArray(acm.testReferences) ? acm.testReferences : [];
39
39
  const testReferences = (rawRefs as unknown[]).map((r) => {
40
40
  const ref = r as Record<string, unknown>;
41
41
  return TestReference.create({
42
- filePath: String(ref['filePath'] ?? ''),
43
- testType: String(ref['testType'] ?? ''),
42
+ filePath: String(ref.filePath ?? ''),
43
+ testType: String(ref.testType ?? ''),
44
44
  });
45
45
  });
46
- return AcMapping.create({ acId: String(acm['acId'] ?? ''), testReferences });
46
+ return AcMapping.create({ acId: String(acm.acId ?? ''), testReferences });
47
47
  });
48
48
 
49
49
  return StoryMapping.create({
50
- storyId: String(story['storyId'] ?? ''),
50
+ storyId: String(story.storyId ?? ''),
51
51
  acMappings,
52
52
  });
53
53
  });
@@ -77,6 +77,6 @@ export class AnalyzeImpactUseCase {
77
77
  const matrix = buildMatrix(rawData);
78
78
  const result = this.impactAnalysisService.analyze(matrix, input.storyId);
79
79
 
80
- return ImpactAnalysisResultMapper.toOutput(result);
80
+ return toAnalyzeImpactOutput(result);
81
81
  }
82
82
  }
@@ -13,7 +13,7 @@ import { RequirementTestMatrix } from '../../domain/aggregates/requirement-test-
13
13
  import { StoryMapping } from '../../domain/entities/story-mapping.js';
14
14
  import { AcMapping } from '../../domain/value-objects/ac-mapping.js';
15
15
  import { TestReference } from '../../domain/value-objects/test-reference.js';
16
- import { CoverageResultMapper } from '../mappers/coverage-result-mapper.js';
16
+ import { toCalculateCoverageOutput } from '../mappers/coverage-result-mapper.js';
17
17
  import type { CalculateCoverageInput } from '../dto/calculate-coverage-input.js';
18
18
  import type { CalculateCoverageOutput } from '../dto/calculate-coverage-output.js';
19
19
 
@@ -27,29 +27,29 @@ export interface CalculateCoverageUseCaseDeps {
27
27
 
28
28
  function buildMatrix(data: unknown): RequirementTestMatrix {
29
29
  const obj = data as Record<string, unknown>;
30
- const rawStories = Array.isArray(obj['stories']) ? obj['stories'] : (Array.isArray(obj['storyMappings']) ? obj['storyMappings'] : []);
30
+ const rawStories = Array.isArray(obj.stories) ? obj.stories : (Array.isArray(obj.storyMappings) ? obj.storyMappings : []);
31
31
 
32
32
  const storyMappings = (rawStories as unknown[]).map((s) => {
33
33
  const story = s as Record<string, unknown>;
34
- const rawAcMappings = Array.isArray(story['storyMappings'])
35
- ? story['storyMappings']
36
- : (Array.isArray(story['acMappings']) ? story['acMappings'] : []);
34
+ const rawAcMappings = Array.isArray(story.storyMappings)
35
+ ? story.storyMappings
36
+ : (Array.isArray(story.acMappings) ? story.acMappings : []);
37
37
 
38
38
  const acMappings = (rawAcMappings as unknown[]).map((a) => {
39
39
  const acm = a as Record<string, unknown>;
40
- const rawRefs = Array.isArray(acm['testReferences']) ? acm['testReferences'] : [];
40
+ const rawRefs = Array.isArray(acm.testReferences) ? acm.testReferences : [];
41
41
  const testReferences = (rawRefs as unknown[]).map((r) => {
42
42
  const ref = r as Record<string, unknown>;
43
43
  return TestReference.create({
44
- filePath: String(ref['filePath'] ?? ''),
45
- testType: String(ref['testType'] ?? ''),
44
+ filePath: String(ref.filePath ?? ''),
45
+ testType: String(ref.testType ?? ''),
46
46
  });
47
47
  });
48
- return AcMapping.create({ acId: String(acm['acId'] ?? ''), testReferences });
48
+ return AcMapping.create({ acId: String(acm.acId ?? ''), testReferences });
49
49
  });
50
50
 
51
51
  return StoryMapping.create({
52
- storyId: String(story['storyId'] ?? ''),
52
+ storyId: String(story.storyId ?? ''),
53
53
  acMappings,
54
54
  });
55
55
  });
@@ -86,6 +86,6 @@ export class CalculateCoverageUseCase {
86
86
  threshold = await this.coverageThresholdPort.getThreshold();
87
87
  }
88
88
 
89
- return CoverageResultMapper.toOutput(coverageResult, threshold);
89
+ return toCalculateCoverageOutput(coverageResult, threshold);
90
90
  }
91
91
  }
@@ -24,29 +24,29 @@ export interface CheckAcCoverageGateUseCaseDeps {
24
24
 
25
25
  function buildMatrix(data: unknown): RequirementTestMatrix {
26
26
  const obj = data as Record<string, unknown>;
27
- const rawStories = Array.isArray(obj['stories']) ? obj['stories'] : (Array.isArray(obj['storyMappings']) ? obj['storyMappings'] : []);
27
+ const rawStories = Array.isArray(obj.stories) ? obj.stories : (Array.isArray(obj.storyMappings) ? obj.storyMappings : []);
28
28
 
29
29
  const storyMappings = (rawStories as unknown[]).map((s) => {
30
30
  const story = s as Record<string, unknown>;
31
- const rawAcMappings = Array.isArray(story['storyMappings'])
32
- ? story['storyMappings']
33
- : (Array.isArray(story['acMappings']) ? story['acMappings'] : []);
31
+ const rawAcMappings = Array.isArray(story.storyMappings)
32
+ ? story.storyMappings
33
+ : (Array.isArray(story.acMappings) ? story.acMappings : []);
34
34
 
35
35
  const acMappings = (rawAcMappings as unknown[]).map((a) => {
36
36
  const acm = a as Record<string, unknown>;
37
- const rawRefs = Array.isArray(acm['testReferences']) ? acm['testReferences'] : [];
37
+ const rawRefs = Array.isArray(acm.testReferences) ? acm.testReferences : [];
38
38
  const testReferences = (rawRefs as unknown[]).map((r) => {
39
39
  const ref = r as Record<string, unknown>;
40
40
  return TestReference.create({
41
- filePath: String(ref['filePath'] ?? ''),
42
- testType: String(ref['testType'] ?? ''),
41
+ filePath: String(ref.filePath ?? ''),
42
+ testType: String(ref.testType ?? ''),
43
43
  });
44
44
  });
45
- return AcMapping.create({ acId: String(acm['acId'] ?? ''), testReferences });
45
+ return AcMapping.create({ acId: String(acm.acId ?? ''), testReferences });
46
46
  });
47
47
 
48
48
  return StoryMapping.create({
49
- storyId: String(story['storyId'] ?? ''),
49
+ storyId: String(story.storyId ?? ''),
50
50
  acMappings,
51
51
  });
52
52
  });
@@ -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
- return `${reference.filePath}\0${reference.testType}\0${reference.testName ?? ''}`;
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
- const generatedReferences = storyReferences.map((reference) => ({
119
- filePath: reference.filePath,
120
- testType: reference.testType,
121
- testName: reference.testName,
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
- version: '1.0',
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) {
@@ -5,7 +5,7 @@
5
5
  * requirement-test-matrix の集約ルート
6
6
  */
7
7
  import { DuplicateStoryMappingError } from '../errors/duplicate-story-mapping-error.js';
8
- import { StoryMapping } from '../entities/story-mapping.js';
8
+ import type { StoryMapping } from '../entities/story-mapping.js';
9
9
 
10
10
  export interface RequirementTestMatrixCreateProps {
11
11
  readonly storyMappings: readonly StoryMapping[];
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * ストーリー単位のAC→テスト参照マッピングエンティティ
6
6
  */
7
- import { AcMapping, type RawAcMapping } from '../value-objects/ac-mapping.js';
7
+ import type { AcMapping, RawAcMapping } from '../value-objects/ac-mapping.js';
8
8
 
9
9
  export interface StoryMappingCreateProps {
10
10
  readonly storyId: string;
@@ -5,7 +5,7 @@
5
5
  * AC網羅ゲート判定ポリシー
6
6
  * validator-system の L3-004 が実行主体として呼び出す公開ポリシー
7
7
  */
8
- import { RequirementTestMatrix } from '../aggregates/requirement-test-matrix.js';
8
+ import type { RequirementTestMatrix } from '../aggregates/requirement-test-matrix.js';
9
9
 
10
10
  export interface NyquistHarnessError {
11
11
  readonly code: string;
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * AC網羅率算出サービス(純粋な算出ロジック)
6
6
  */
7
- import { RequirementTestMatrix } from '../aggregates/requirement-test-matrix.js';
7
+ import type { RequirementTestMatrix } from '../aggregates/requirement-test-matrix.js';
8
8
  import { CoverageResult } from '../value-objects/coverage-result.js';
9
9
 
10
10
  export class CoverageCalculationService {
@@ -4,9 +4,9 @@
4
4
  *
5
5
  * テストケース逆引きサービス(v1: 直接マッピングのみ)
6
6
  */
7
- import { RequirementTestMatrix } from '../aggregates/requirement-test-matrix.js';
7
+ import type { RequirementTestMatrix } from '../aggregates/requirement-test-matrix.js';
8
8
  import { ImpactAnalysisResult } from '../value-objects/impact-analysis-result.js';
9
- import { TestReference } from '../value-objects/test-reference.js';
9
+ import type { TestReference } from '../value-objects/test-reference.js';
10
10
 
11
11
  export class ImpactAnalysisService {
12
12
  analyze(matrix: RequirementTestMatrix, storyId: string): ImpactAnalysisResult {