phasegate 0.150.2 → 0.151.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 (25) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/phasegate +7 -2
  3. package/package.json +1 -1
  4. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +1 -1
  5. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +1 -0
  6. package/scripts/harness/phase2-extensions/application/dto/validate-doc-pointers-output.ts +1 -0
  7. package/scripts/harness/phase2-extensions/application/usecases/validate-doc-pointers-usecase.ts +1 -0
  8. package/scripts/harness/phase2-extensions/domain/aggregates/pointer-rule.ts +1 -0
  9. package/scripts/harness/phase2-extensions/domain/services/freshness-check-service.ts +1 -0
  10. package/scripts/harness/phase2-extensions/domain/value-objects/document-age.ts +1 -0
  11. package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +1 -0
  12. package/scripts/harness/phase2-extensions/presentation/formatters/pointer-result-formatter.ts +1 -0
  13. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +29 -1
  14. package/scripts/harness/validator-system/composition-root.ts +6 -2
  15. package/scripts/harness/validator-system/domain/ports/contract-traceability-policy-port.ts +9 -0
  16. package/scripts/harness/validator-system/domain/services/contract-traceability-coverage-service.ts +259 -0
  17. package/scripts/harness/validator-system/domain/services/l4/consistency-check-service.ts +1 -0
  18. package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +1 -0
  19. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +1 -0
  20. package/scripts/harness/validator-system/domain/value-objects/contract-traceability-model.ts +124 -0
  21. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +2 -0
  22. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +1 -0
  23. package/scripts/harness/validator-system/infrastructure/adapters/file-system-contract-traceability-policy-adapter.ts +115 -0
  24. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +1 -1
  25. package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.151.1] - 2026-05-12
11
+
12
+ ### Fixed
13
+
14
+ - **G4 post-publish dogfood** — fixes `npx phasegate@0.151.0` failing with `Error: tsx not found` by letting the bin wrapper execute the packaged `tsx` loader via `node --import` when dependency binaries are not linked into PATH.
15
+
16
+ ## [0.151.0] - 2026-05-12
17
+
18
+ ### Added
19
+
20
+ - **G4 / WI-132 / WI-133 / WI-136 / WI-137 / WI-138 — contract, boundary, state machine, error contract, and traceability coverage** — adds `L2-015 contract-traceability-coverage`, a semantic model for public contracts, test observations, boundary cases, state machines, error contracts, and traceability graph slices, plus opt-in `@phasegate-contract` / `@phasegate-observation` extraction and L2 result mapping.
21
+
10
22
  ## [0.150.2] - 2026-05-12
11
23
 
12
24
  ### Added
package/bin/phasegate CHANGED
@@ -7,8 +7,13 @@ SCRIPT_PATH="$(realpath "$0" 2>/dev/null || readlink -f "$0" 2>/dev/null || echo
7
7
  PACKAGE_DIR="$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)"
8
8
  MAIN_TS="$PACKAGE_DIR/scripts/harness/main.ts"
9
9
 
10
- # tsx を探す: npx経由の場合はnode_modules/.binがPATHに入っているので command -v で見つかる
11
- if command -v tsx >/dev/null 2>&1; then
10
+ # tsx を探す。npm/npx の一時インストールでは dependency bin PATH ../.bin
11
+ # 出ないことがあるため、tsx loader を直接 node --import できる経路も見る。
12
+ if [ -f "$PACKAGE_DIR/node_modules/tsx/dist/loader.mjs" ]; then
13
+ exec node --import "$PACKAGE_DIR/node_modules/tsx/dist/loader.mjs" "$MAIN_TS" "$@"
14
+ elif [ -f "$PACKAGE_DIR/../tsx/dist/loader.mjs" ]; then
15
+ exec node --import "$PACKAGE_DIR/../tsx/dist/loader.mjs" "$MAIN_TS" "$@"
16
+ elif command -v tsx >/dev/null 2>&1; then
12
17
  exec tsx "$MAIN_TS" "$@"
13
18
  elif [ -f "$PACKAGE_DIR/node_modules/.bin/tsx" ]; then
14
19
  exec "$PACKAGE_DIR/node_modules/.bin/tsx" "$MAIN_TS" "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.150.2",
3
+ "version": "0.151.1",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -11,7 +11,7 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
11
11
  return {
12
12
  project: { preset: resolvedConfig.project.preset },
13
13
  layers: {
14
- L2: { enabled: resolvedConfig.layers.L2.enabled, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014'] },
14
+ L2: { enabled: resolvedConfig.layers.L2.enabled, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'] },
15
15
  L3: { enabled: resolvedConfig.layers.L3.enabled },
16
16
  L4: { enabled: resolvedConfig.layers.L4.enabled, validators: resolvedConfig.layers.L4.validators },
17
17
  },
@@ -1,4 +1,5 @@
1
1
  // @layer domain
2
+ // @unit harness-api
2
3
  // harness-status-summary.ts — HarnessStatusSummary Value Object
3
4
 
4
5
  import type { LayerHealth, LayerId } from './layer-health.js';
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit phase2-extensions
4
+ * @work-item-id WI-122
4
5
  */
5
6
  import type { HarnessErrorContract } from '../../../harness-error/application/dto/harness-error-contract.js';
6
7
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit phase2-extensions
4
+ * @work-item-id WI-122
4
5
  */
5
6
  import type { FreshnessConfigPort } from '../../domain/ports/freshness-config-port.js';
6
7
  import type { DocumentScannerPort } from '../../domain/ports/document-scanner-port.js';
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit phase2-extensions
4
+ * @work-item-id WI-122
4
5
  */
5
6
  import { Phase2ExtensionsDomainError } from '../errors/phase2-extensions-domain-error.js';
6
7
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit phase2-extensions
4
+ * @work-item-id WI-122
4
5
  */
5
6
  import type { DocFreshnessRule } from '../aggregates/doc-freshness-rule.js';
6
7
  import type { DocumentAge, DocumentAgeSource } from '../value-objects/document-age.js';
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit phase2-extensions
4
+ * @work-item-id WI-122
4
5
  */
5
6
  import { Phase2ExtensionsDomainError } from '../errors/phase2-extensions-domain-error.js';
6
7
 
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit phase2-extensions
4
+ * @work-item-id WI-122
4
5
  */
5
6
  import type { HarnessConfigV2 } from '../../../config-foundation/domain/harness-config.js';
6
7
  import { DocFreshnessRule } from '../../domain/aggregates/doc-freshness-rule.js';
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer presentation
3
3
  * @unit phase2-extensions
4
+ * @work-item-id WI-122
4
5
  */
5
6
  import type { ValidateDocPointersOutput } from '../../application/dto/validate-doc-pointers-output.js';
6
7
 
@@ -3,7 +3,7 @@
3
3
  * @unit validator-system
4
4
  *
5
5
  * RunL2ValidatorsUseCase — H08-01: L2バリデータ実行
6
- * @work-item-id WI-110 / WI-111 / WI-140
6
+ * @work-item-id WI-110 / WI-111 / WI-140 / WI-132 / WI-133 / WI-136 / WI-137 / WI-138
7
7
  */
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { ValidatorId, InvalidValidatorIdError } from '../../domain/value-objects/validator-id.js';
@@ -21,6 +21,8 @@ import type { CliCommandRegistryPort } from '../../domain/ports/cli-command-regi
21
21
  import type { E2eTestFileRegistryPort } from '../../domain/ports/e2e-test-file-registry-port.js';
22
22
  import { CliE2eTestExistenceService } from '../../domain/services/cli-e2e-test-existence-service.js';
23
23
  import type { WorkItemStatusPolicyPort } from '../../domain/ports/work-item-status-policy-port.js';
24
+ import type { ContractTraceabilityPolicyPort } from '../../domain/ports/contract-traceability-policy-port.js';
25
+ import { ContractTraceabilityCoverageService } from '../../domain/services/contract-traceability-coverage-service.js';
24
26
 
25
27
  export interface RunL2ValidatorsUseCaseDeps {
26
28
  validatorRegistry: ValidatorRegistry;
@@ -33,6 +35,7 @@ export interface RunL2ValidatorsUseCaseDeps {
33
35
  cliCommandRegistryPort?: CliCommandRegistryPort;
34
36
  e2eTestFileRegistryPort?: E2eTestFileRegistryPort;
35
37
  workItemStatusPolicyPort?: WorkItemStatusPolicyPort;
38
+ contractTraceabilityPolicyPort?: ContractTraceabilityPolicyPort;
36
39
  }
37
40
 
38
41
  export class RunL2ValidatorsUseCase {
@@ -46,7 +49,9 @@ export class RunL2ValidatorsUseCase {
46
49
  private readonly cliCommandRegistryPort?: CliCommandRegistryPort;
47
50
  private readonly e2eTestFileRegistryPort?: E2eTestFileRegistryPort;
48
51
  private readonly workItemStatusPolicyPort?: WorkItemStatusPolicyPort;
52
+ private readonly contractTraceabilityPolicyPort?: ContractTraceabilityPolicyPort;
49
53
  private readonly cliE2eTestExistenceService = new CliE2eTestExistenceService();
54
+ private readonly contractTraceabilityCoverageService = new ContractTraceabilityCoverageService();
50
55
 
51
56
  constructor(deps: RunL2ValidatorsUseCaseDeps) {
52
57
  this.registry = deps.validatorRegistry;
@@ -59,6 +64,7 @@ export class RunL2ValidatorsUseCase {
59
64
  this.cliCommandRegistryPort = deps.cliCommandRegistryPort;
60
65
  this.e2eTestFileRegistryPort = deps.e2eTestFileRegistryPort;
61
66
  this.workItemStatusPolicyPort = deps.workItemStatusPolicyPort;
67
+ this.contractTraceabilityPolicyPort = deps.contractTraceabilityPolicyPort;
62
68
  }
63
69
 
64
70
  async execute(input: RunL2ValidatorsInput): Promise<readonly ValidationResultContract[]> {
@@ -180,6 +186,28 @@ export class RunL2ValidatorsUseCase {
180
186
  }
181
187
  }
182
188
 
189
+ if (this.contractTraceabilityPolicyPort) {
190
+ const l2015Result = overrideMap.get('L2-015');
191
+ if (l2015Result && !l2015Result.skipped) {
192
+ const inputModel = await this.contractTraceabilityPolicyPort.collect(input.targetPaths);
193
+ const report = this.contractTraceabilityCoverageService.check(inputModel);
194
+ if (report.hasFindings()) {
195
+ const errors = report.findings.map((finding) => ({
196
+ code: { value: 'L2-015', toString: () => 'L2-015' },
197
+ severity: { value: finding.severity, toString: () => finding.severity },
198
+ message: finding.message,
199
+ suggestion: finding.suggestion,
200
+ kind: finding.kind,
201
+ subject: finding.subject,
202
+ sourcePath: finding.sourcePath,
203
+ }));
204
+ overrideMap.set('L2-015', ValidationResult.fail(ValidatorId.create('L2-015'), errors, 0));
205
+ } else {
206
+ overrideMap.set('L2-015', ValidationResult.pass(ValidatorId.create('L2-015'), 0));
207
+ }
208
+ }
209
+ }
210
+
183
211
  const finalResults = definitions.map(
184
212
  (definition) => overrideMap.get(definition.validatorId.value) ?? ValidationResult.skip(definition.validatorId),
185
213
  );
@@ -3,7 +3,7 @@
3
3
  * @unit validator-system
4
4
  *
5
5
  * DI 組み立て — validator-system の全依存関係を構築する
6
- * @work-item-id WI-110 / WI-111
6
+ * @work-item-id WI-110 / WI-111 / WI-132 / WI-133 / WI-136 / WI-137 / WI-138
7
7
  */
8
8
  import { ValidatorId } from './domain/value-objects/validator-id.js';
9
9
  import { ValidatorDefinition } from './domain/value-objects/validator-definition.js';
@@ -24,6 +24,7 @@ import { SourceFileTextScannerAdapter } from './infrastructure/adapters/source-f
24
24
  import { E2eTestFileRegistryAdapter } from './infrastructure/adapters/e2e-test-file-registry-adapter.js';
25
25
  import { CliCommandRegistryAdapter } from './infrastructure/adapters/cli-command-registry-adapter.js';
26
26
  import { TraceabilityWorkItemStatusPolicyAdapter } from './infrastructure/adapters/traceability-work-item-status-policy-adapter.js';
27
+ import { FileSystemContractTraceabilityPolicyAdapter } from './infrastructure/adapters/file-system-contract-traceability-policy-adapter.js';
27
28
  import { PhaseDependencyPhaseGatePolicyAdapter } from './infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.js';
28
29
  import { TraceabilityMetadataPolicyAdapter } from './infrastructure/adapters/traceability-metadata-policy-adapter.js';
29
30
  import { NyquistAcCoveragePolicyAdapter } from './infrastructure/adapters/nyquist-ac-coverage-policy-adapter.js';
@@ -47,7 +48,7 @@ import { join } from 'node:path';
47
48
  const DEFAULT_CONFIG = {
48
49
  preset: 'standard' as const,
49
50
  layers: {
50
- L2: { enabled: true, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014'] },
51
+ L2: { enabled: true, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'] },
51
52
  L3: { enabled: true, validators: ['L3-001', 'L3-002', 'L3-003', 'L3-004'], coverageThreshold: 90, bundleSizeLimit: 512000 },
52
53
  L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'] },
53
54
  },
@@ -81,6 +82,7 @@ export function buildDefaultRegistry(): ValidatorRegistry {
81
82
  createDef('L2-003', 'L2', 'always'),
82
83
  createDef('L2-013', 'L2', 'always', 'CliE2eTestExistenceService'),
83
84
  createDef('L2-014', 'L2', 'always', 'WorkItemStatusPolicyPort'),
85
+ createDef('L2-015', 'L2', 'always', 'ContractTraceabilityPolicyPort'),
84
86
  createDef('L3-001', 'L3', 'always'),
85
87
  createDef('L3-002', 'L3', 'strictOnly'),
86
88
  createDef('L3-003', 'L3', 'always'),
@@ -127,6 +129,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
127
129
  const securityScannerPort = new FileSystemSecurityPatternScannerAdapter();
128
130
  const performanceScannerPort = new AstPerformanceScannerAdapter();
129
131
  const workItemStatusPolicyPort = new TraceabilityWorkItemStatusPolicyAdapter(process.cwd());
132
+ const contractTraceabilityPolicyPort = new FileSystemContractTraceabilityPolicyAdapter();
130
133
 
131
134
  const docsRoot = join(process.cwd(), 'docs/product/construction');
132
135
  const cwd = process.cwd();
@@ -151,6 +154,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
151
154
  e2eTestFileRegistryPort,
152
155
  cliCommandRegistryPort,
153
156
  workItemStatusPolicyPort,
157
+ contractTraceabilityPolicyPort,
154
158
  });
155
159
 
156
160
  const runL3ValidatorsUseCase = new RunL3ValidatorsUseCase({
@@ -0,0 +1,9 @@
1
+ // @unit validator-system
2
+ // @layer domain
3
+ // @work-item-id WI-132 / WI-133 / WI-136 / WI-137 / WI-138
4
+
5
+ import type { ContractTraceabilityInput } from '../value-objects/contract-traceability-model.js';
6
+
7
+ export interface ContractTraceabilityPolicyPort {
8
+ collect(targetPaths: readonly string[]): Promise<ContractTraceabilityInput>;
9
+ }
@@ -0,0 +1,259 @@
1
+ // @unit validator-system
2
+ // @layer domain
3
+ // @work-item-id WI-132 / WI-133 / WI-136 / WI-137 / WI-138
4
+
5
+ import {
6
+ type BoundaryCaseKind,
7
+ type ContractTraceabilityFinding,
8
+ type ContractTraceabilityInput,
9
+ ContractTraceabilityReport,
10
+ type ErrorContract,
11
+ type PublicContract,
12
+ type StateMachineModel,
13
+ type TestObservation,
14
+ type TraceabilityGraphSlice,
15
+ } from '../value-objects/contract-traceability-model.js';
16
+
17
+ export class ContractTraceabilityCoverageService {
18
+ check(input: ContractTraceabilityInput): ContractTraceabilityReport {
19
+ return ContractTraceabilityReport.create([
20
+ ...this.checkPublicContracts(input.publicContracts, input.testObservations),
21
+ ...this.checkErrorContracts(input.errorContracts, input.testObservations),
22
+ ...this.checkStateMachines(input.stateMachines, input.testObservations),
23
+ ...this.checkTraceability(input.traceabilitySlices),
24
+ ]);
25
+ }
26
+
27
+ private checkPublicContracts(
28
+ contracts: readonly PublicContract[],
29
+ observations: readonly TestObservation[],
30
+ ): readonly ContractTraceabilityFinding[] {
31
+ const findings: ContractTraceabilityFinding[] = [];
32
+ for (const contract of contracts) {
33
+ for (const behavior of contract.requiredBehaviors) {
34
+ if (!this.isCovered(`${contract.id}:${behavior}`, observations)) {
35
+ findings.push(this.finding(
36
+ 'missing-required-behavior-test',
37
+ contract.id,
38
+ contract.sourcePath,
39
+ `Public contract ${contract.id} requires behavior "${behavior}" but no matching test observation covers it.`,
40
+ `Add a test observation covering ${contract.id}:${behavior}.`,
41
+ ));
42
+ }
43
+ }
44
+
45
+ if (contract.kind === 'port' && !observations.some((observation) => (
46
+ observation.kind === 'adapter-contract' && observation.covers.includes(contract.id)
47
+ ))) {
48
+ findings.push(this.finding(
49
+ 'missing-port-contract-test',
50
+ contract.id,
51
+ contract.sourcePath,
52
+ `Port contract ${contract.id} has no adapter contract test observation.`,
53
+ `Add an adapter contract test that covers ${contract.id}.`,
54
+ ));
55
+ }
56
+
57
+ for (const boundaryCase of contract.boundaryCases ?? []) {
58
+ if (!this.isBoundaryCovered(contract, boundaryCase, observations)) {
59
+ findings.push(this.finding(
60
+ 'missing-boundary-test',
61
+ contract.id,
62
+ contract.sourcePath,
63
+ `Contract ${contract.id} requires boundary case "${boundaryCase}" but no matching test observation covers it.`,
64
+ `Add a boundary test observation covering ${contract.id}:boundary:${boundaryCase}.`,
65
+ ));
66
+ }
67
+ }
68
+ }
69
+ return findings;
70
+ }
71
+
72
+ private checkErrorContracts(
73
+ contracts: readonly ErrorContract[],
74
+ observations: readonly TestObservation[],
75
+ ): readonly ContractTraceabilityFinding[] {
76
+ const findings: ContractTraceabilityFinding[] = [];
77
+ for (const contract of contracts) {
78
+ const missingShapeFields = [
79
+ ['code', contract.code],
80
+ ['severity', contract.severity],
81
+ ['message', contract.message],
82
+ ['suggestion', contract.suggestion],
83
+ ['documentationRef', contract.documentationRef],
84
+ ].filter(([, value]) => !this.hasUsefulText(value));
85
+
86
+ if (missingShapeFields.length > 0) {
87
+ findings.push(this.finding(
88
+ 'error-contract-shape',
89
+ contract.id,
90
+ contract.sourcePath,
91
+ `Error contract ${contract.id} is missing required fields: ${missingShapeFields.map(([field]) => field).join(', ')}.`,
92
+ 'Provide stable code, severity, message, suggestion, and documentation reference.',
93
+ ));
94
+ }
95
+
96
+ if (this.isGenericSuggestion(contract.suggestion)) {
97
+ findings.push(this.finding(
98
+ 'error-contract-shape',
99
+ contract.id,
100
+ contract.sourcePath,
101
+ `Error contract ${contract.id} has a generic or empty recovery suggestion.`,
102
+ 'Replace the suggestion with a concrete next action.',
103
+ ));
104
+ }
105
+
106
+ if (contract.exitCode !== undefined && contract.severity !== undefined) {
107
+ const expected = contract.severity === 'error' ? [1, 2] : [0];
108
+ if (!expected.includes(contract.exitCode)) {
109
+ findings.push(this.finding(
110
+ 'error-contract-exit-code',
111
+ contract.id,
112
+ contract.sourcePath,
113
+ `Error contract ${contract.id} severity=${contract.severity} is inconsistent with exitCode=${contract.exitCode}.`,
114
+ 'Align warning contracts with exit 0 and error contracts with exit 1 or 2.',
115
+ ));
116
+ }
117
+ }
118
+
119
+ if (!this.isCovered(`${contract.id}:error-path`, observations)) {
120
+ findings.push(this.finding(
121
+ 'missing-error-path-test',
122
+ contract.id,
123
+ contract.sourcePath,
124
+ `Error contract ${contract.id} has no error path test observation.`,
125
+ `Add a test observation covering ${contract.id}:error-path.`,
126
+ ));
127
+ }
128
+ }
129
+ return findings;
130
+ }
131
+
132
+ private checkStateMachines(
133
+ machines: readonly StateMachineModel[],
134
+ observations: readonly TestObservation[],
135
+ ): readonly ContractTraceabilityFinding[] {
136
+ const findings: ContractTraceabilityFinding[] = [];
137
+ for (const machine of machines) {
138
+ const codeStates = new Set(machine.codeStates);
139
+ const docsStates = new Set(machine.docsStates);
140
+ const mismatchedStates = [
141
+ ...machine.docsStates.filter((state) => !codeStates.has(state)),
142
+ ...machine.codeStates.filter((state) => !docsStates.has(state)),
143
+ ];
144
+ if (mismatchedStates.length > 0) {
145
+ findings.push(this.finding(
146
+ 'state-doc-code-mismatch',
147
+ machine.id,
148
+ machine.sourcePath,
149
+ `State machine ${machine.id} has docs/code state mismatch: ${mismatchedStates.join(', ')}.`,
150
+ 'Update docs and code state definitions so they describe the same states.',
151
+ ));
152
+ }
153
+
154
+ for (const transition of machine.invalidTransitions) {
155
+ if (machine.terminalStates.includes(transition.from)) {
156
+ findings.push(this.finding(
157
+ 'state-invalid-terminal-transition',
158
+ machine.id,
159
+ machine.sourcePath,
160
+ `State machine ${machine.id} defines invalid transition from terminal state ${transition.from} to ${transition.to}.`,
161
+ 'Remove terminal-state outgoing transitions or mark them as rejected behavior.',
162
+ ));
163
+ }
164
+ }
165
+
166
+ for (const transition of machine.transitions) {
167
+ const key = `${machine.id}:transition:${transition.from}->${transition.to}`;
168
+ if (!this.isCovered(key, observations)) {
169
+ findings.push(this.finding(
170
+ 'missing-transition-test',
171
+ machine.id,
172
+ machine.sourcePath,
173
+ `State transition ${transition.from}->${transition.to} has no success/failure test observation.`,
174
+ `Add a test observation covering ${key}.`,
175
+ ));
176
+ }
177
+ }
178
+ }
179
+ return findings;
180
+ }
181
+
182
+ private checkTraceability(slices: readonly TraceabilityGraphSlice[]): readonly ContractTraceabilityFinding[] {
183
+ const findings: ContractTraceabilityFinding[] = [];
184
+ for (const slice of slices) {
185
+ const productUnits = new Set(slice.productUnits);
186
+ const missingUnits = slice.affectedUnits.filter((unit) => !productUnits.has(unit));
187
+ if (missingUnits.length > 0) {
188
+ findings.push(this.finding(
189
+ 'traceability-unit-mismatch',
190
+ slice.workItemId,
191
+ slice.workItemId,
192
+ `${slice.workItemId} affects units without product reflection: ${missingUnits.join(', ')}.`,
193
+ `Reflect @work-item-id ${slice.workItemId} in product docs for each affected unit.`,
194
+ ));
195
+ }
196
+
197
+ if (slice.implementationWorkItemIds.includes(slice.workItemId) && !slice.testWorkItemIds.includes(slice.workItemId)) {
198
+ findings.push(this.finding(
199
+ 'traceability-test-mismatch',
200
+ slice.workItemId,
201
+ slice.workItemId,
202
+ `${slice.workItemId} has implementation evidence but no matching test observation.`,
203
+ `Add tests annotated with @work-item-id ${slice.workItemId}.`,
204
+ ));
205
+ }
206
+
207
+ if (slice.publicDocsChanged !== slice.contractChanged) {
208
+ findings.push(this.finding(
209
+ 'public-doc-contract-sync',
210
+ slice.workItemId,
211
+ slice.workItemId,
212
+ `${slice.workItemId} public docs and public contract changes are not synchronized.`,
213
+ 'Update public docs and contract declarations in the same WI, or document why only one changed.',
214
+ ));
215
+ }
216
+ }
217
+ return findings;
218
+ }
219
+
220
+ private isBoundaryCovered(
221
+ contract: PublicContract,
222
+ boundaryCase: BoundaryCaseKind,
223
+ observations: readonly TestObservation[],
224
+ ): boolean {
225
+ return this.isCovered(`${contract.id}:boundary:${boundaryCase}`, observations)
226
+ || this.isCovered(`${contract.id}:${boundaryCase}`, observations);
227
+ }
228
+
229
+ private isCovered(requiredKey: string, observations: readonly TestObservation[]): boolean {
230
+ return observations.some((observation) => observation.covers.includes(requiredKey));
231
+ }
232
+
233
+ private hasUsefulText(value: unknown): boolean {
234
+ return typeof value === 'string' && value.trim().length > 0;
235
+ }
236
+
237
+ private isGenericSuggestion(value: unknown): boolean {
238
+ if (!this.hasUsefulText(value)) return true;
239
+ const normalized = String(value).trim().toLowerCase();
240
+ return ['fix it', 'check the error', 'see logs', 'unknown'].includes(normalized);
241
+ }
242
+
243
+ private finding(
244
+ kind: ContractTraceabilityFinding['kind'],
245
+ subject: string,
246
+ sourcePath: string,
247
+ message: string,
248
+ suggestion: string,
249
+ ): ContractTraceabilityFinding {
250
+ return {
251
+ kind,
252
+ severity: 'error',
253
+ subject,
254
+ sourcePath,
255
+ message,
256
+ suggestion,
257
+ };
258
+ }
259
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit validator-system
4
+ * @work-item-id WI-118
4
5
  *
5
6
  * ConsistencyCheckService ドメインサービス
6
7
  * 設計文書間のレイヤー整合性検証(L4-002)
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit validator-system
4
+ * @work-item-id WI-117
4
5
  *
5
6
  * DriftDetectionService ドメインサービス
6
7
  * 設計文書(domain_model.md等)とソースコード実装の双方向乖離検出(L4-001)
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit validator-system
4
+ * @work-item-id WI-118
4
5
  *
5
6
  * ConsistencyReport 値オブジェクト
6
7
  * 設計文書間のレイヤー整合性検証結果VO(L4-002専用)
@@ -0,0 +1,124 @@
1
+ // @unit validator-system
2
+ // @layer domain
3
+ // @work-item-id WI-132 / WI-133 / WI-136 / WI-137 / WI-138
4
+
5
+ export type PublicContractKind =
6
+ | 'cli-command'
7
+ | 'api-endpoint'
8
+ | 'port'
9
+ | 'config-option'
10
+ | 'domain-behavior'
11
+ | 'error-code';
12
+
13
+ export type BoundaryCaseKind =
14
+ | 'empty-input'
15
+ | 'missing-required'
16
+ | 'invalid-enum'
17
+ | 'duplicate-id'
18
+ | 'unknown-reference'
19
+ | 'permission-denied'
20
+ | 'config-disabled'
21
+ | 'partial-failure'
22
+ | 'idempotency'
23
+ | 'backward-compatibility';
24
+
25
+ export interface PublicContract {
26
+ readonly id: string;
27
+ readonly kind: PublicContractKind;
28
+ readonly sourcePath: string;
29
+ readonly requiredBehaviors: readonly string[];
30
+ readonly boundaryCases?: readonly BoundaryCaseKind[];
31
+ }
32
+
33
+ export interface TestObservation {
34
+ readonly id: string;
35
+ readonly kind: 'unit' | 'integration' | 'e2e' | 'adapter-contract';
36
+ readonly sourcePath: string;
37
+ readonly covers: readonly string[];
38
+ }
39
+
40
+ export interface ErrorContract {
41
+ readonly id: string;
42
+ readonly sourcePath: string;
43
+ readonly code?: string;
44
+ readonly severity?: 'error' | 'warning';
45
+ readonly message?: string;
46
+ readonly suggestion?: string;
47
+ readonly documentationRef?: string;
48
+ readonly exitCode?: number;
49
+ readonly machineFields?: readonly string[];
50
+ }
51
+
52
+ export interface StateTransition {
53
+ readonly from: string;
54
+ readonly to: string;
55
+ readonly guard?: string;
56
+ }
57
+
58
+ export interface StateMachineModel {
59
+ readonly id: string;
60
+ readonly sourcePath: string;
61
+ readonly docsStates: readonly string[];
62
+ readonly codeStates: readonly string[];
63
+ readonly transitions: readonly StateTransition[];
64
+ readonly terminalStates: readonly string[];
65
+ readonly invalidTransitions: readonly StateTransition[];
66
+ }
67
+
68
+ export interface TraceabilityGraphSlice {
69
+ readonly workItemId: string;
70
+ readonly affectedUnits: readonly string[];
71
+ readonly productUnits: readonly string[];
72
+ readonly implementationWorkItemIds: readonly string[];
73
+ readonly testWorkItemIds: readonly string[];
74
+ readonly publicDocsChanged: boolean;
75
+ readonly contractChanged: boolean;
76
+ }
77
+
78
+ export interface ContractTraceabilityInput {
79
+ readonly publicContracts: readonly PublicContract[];
80
+ readonly testObservations: readonly TestObservation[];
81
+ readonly errorContracts: readonly ErrorContract[];
82
+ readonly stateMachines: readonly StateMachineModel[];
83
+ readonly traceabilitySlices: readonly TraceabilityGraphSlice[];
84
+ }
85
+
86
+ export type ContractTraceabilityFindingKind =
87
+ | 'missing-required-behavior-test'
88
+ | 'missing-port-contract-test'
89
+ | 'missing-boundary-test'
90
+ | 'error-contract-shape'
91
+ | 'error-contract-exit-code'
92
+ | 'missing-error-path-test'
93
+ | 'state-doc-code-mismatch'
94
+ | 'state-invalid-terminal-transition'
95
+ | 'missing-transition-test'
96
+ | 'traceability-unit-mismatch'
97
+ | 'traceability-test-mismatch'
98
+ | 'public-doc-contract-sync';
99
+
100
+ export interface ContractTraceabilityFinding {
101
+ readonly kind: ContractTraceabilityFindingKind;
102
+ readonly severity: 'error' | 'warning';
103
+ readonly subject: string;
104
+ readonly sourcePath: string;
105
+ readonly message: string;
106
+ readonly suggestion: string;
107
+ }
108
+
109
+ export class ContractTraceabilityReport {
110
+ readonly findings: readonly ContractTraceabilityFinding[];
111
+
112
+ private constructor(findings: readonly ContractTraceabilityFinding[]) {
113
+ this.findings = Object.freeze([...findings]);
114
+ Object.freeze(this);
115
+ }
116
+
117
+ static create(findings: readonly ContractTraceabilityFinding[]): ContractTraceabilityReport {
118
+ return new ContractTraceabilityReport(findings);
119
+ }
120
+
121
+ hasFindings(): boolean {
122
+ return this.findings.length > 0;
123
+ }
124
+ }
@@ -7,6 +7,7 @@
7
7
  * L1-001〜L4-005 のバリデータを識別する不変値オブジェクト
8
8
  * Wave 2A で L1-017, L1-018, L2-013 を追加
9
9
  * WI-140 で L2-014 を追加
10
+ * WI-132/WI-133/WI-136/WI-137/WI-138 で L2-015 を追加
10
11
  */
11
12
 
12
13
  export class InvalidValidatorIdError extends Error {
@@ -29,6 +30,7 @@ const VALIDATOR_NAME_MAP: Record<string, string> = {
29
30
  'L2-003': 'test-quality',
30
31
  'L2-013': 'cli-e2e-test-existence',
31
32
  'L2-014': 'work-item-status-staleness',
33
+ 'L2-015': 'contract-traceability-coverage',
32
34
  'L3-001': 'security',
33
35
  'L3-002': 'performance',
34
36
  'L3-003': 'coverage',
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit validator-system
4
+ * @work-item-id WI-117
4
5
  *
5
6
  * BiomeAstSourceCodeAnalyzerAdapter — SourceCodeAnalyzerPort実装
6
7
  * TypeScript Compiler API を使用してエクスポートを正確に抽出する(L4-001, L4-003)
@@ -0,0 +1,115 @@
1
+ // @unit validator-system
2
+ // @layer infrastructure
3
+ // @work-item-id WI-132 / WI-133 / WI-136 / WI-137 / WI-138
4
+
5
+ import { readFile } from 'node:fs/promises';
6
+ import type { ContractTraceabilityPolicyPort } from '../../domain/ports/contract-traceability-policy-port.js';
7
+ import type {
8
+ BoundaryCaseKind,
9
+ ContractTraceabilityInput,
10
+ PublicContract,
11
+ PublicContractKind,
12
+ TestObservation,
13
+ } from '../../domain/value-objects/contract-traceability-model.js';
14
+
15
+ const CONTRACT_KINDS = new Set<PublicContractKind>([
16
+ 'cli-command',
17
+ 'api-endpoint',
18
+ 'port',
19
+ 'config-option',
20
+ 'domain-behavior',
21
+ 'error-code',
22
+ ]);
23
+
24
+ const BOUNDARY_KINDS = new Set<BoundaryCaseKind>([
25
+ 'empty-input',
26
+ 'missing-required',
27
+ 'invalid-enum',
28
+ 'duplicate-id',
29
+ 'unknown-reference',
30
+ 'permission-denied',
31
+ 'config-disabled',
32
+ 'partial-failure',
33
+ 'idempotency',
34
+ 'backward-compatibility',
35
+ ]);
36
+
37
+ export class FileSystemContractTraceabilityPolicyAdapter implements ContractTraceabilityPolicyPort {
38
+ async collect(targetPaths: readonly string[]): Promise<ContractTraceabilityInput> {
39
+ const publicContracts: PublicContract[] = [];
40
+ const testObservations: TestObservation[] = [];
41
+
42
+ for (const targetPath of targetPaths) {
43
+ let content: string;
44
+ try {
45
+ content = await readFile(targetPath, 'utf-8');
46
+ } catch {
47
+ continue;
48
+ }
49
+
50
+ publicContracts.push(...this.extractContracts(targetPath, content));
51
+ testObservations.push(...this.extractObservations(targetPath, content));
52
+ }
53
+
54
+ return {
55
+ publicContracts,
56
+ testObservations,
57
+ errorContracts: [],
58
+ stateMachines: [],
59
+ traceabilitySlices: [],
60
+ };
61
+ }
62
+
63
+ private extractContracts(sourcePath: string, content: string): readonly PublicContract[] {
64
+ return [...content.matchAll(/@phasegate-contract\s+([^\n\r*]+)/g)].flatMap((match) => {
65
+ const attrs = this.parseAttrs(match[1]);
66
+ const id = attrs.get('id');
67
+ const kind = attrs.get('kind');
68
+ if (!id || !this.isContractKind(kind)) return [];
69
+
70
+ return [{
71
+ id,
72
+ kind,
73
+ sourcePath,
74
+ requiredBehaviors: this.splitList(attrs.get('behaviors')),
75
+ boundaryCases: this.splitList(attrs.get('boundary')).filter(this.isBoundaryKind),
76
+ }];
77
+ });
78
+ }
79
+
80
+ private extractObservations(sourcePath: string, content: string): readonly TestObservation[] {
81
+ return [...content.matchAll(/@phasegate-observation\s+([^\n\r*]+)/g)].flatMap((match, index) => {
82
+ const attrs = this.parseAttrs(match[1]);
83
+ const covers = this.splitList(attrs.get('covers'));
84
+ if (covers.length === 0) return [];
85
+ const kind = attrs.get('kind');
86
+ return [{
87
+ id: attrs.get('id') ?? `${sourcePath}#observation-${index + 1}`,
88
+ kind: kind === 'adapter-contract' || kind === 'integration' || kind === 'e2e' ? kind : 'unit',
89
+ sourcePath,
90
+ covers,
91
+ }];
92
+ });
93
+ }
94
+
95
+ private parseAttrs(raw: string): Map<string, string> {
96
+ const attrs = new Map<string, string>();
97
+ for (const match of raw.matchAll(/([a-zA-Z][a-zA-Z0-9_-]*)=("[^"]*"|'[^']*'|[^\s]+)/g)) {
98
+ attrs.set(match[1], match[2].replace(/^['"]|['"]$/g, ''));
99
+ }
100
+ return attrs;
101
+ }
102
+
103
+ private splitList(raw: string | undefined): string[] {
104
+ if (!raw) return [];
105
+ return raw.split(',').map((value) => value.trim()).filter((value) => value.length > 0);
106
+ }
107
+
108
+ private isContractKind(value: string | undefined): value is PublicContractKind {
109
+ return value !== undefined && CONTRACT_KINDS.has(value as PublicContractKind);
110
+ }
111
+
112
+ private isBoundaryKind(value: string): value is BoundaryCaseKind {
113
+ return BOUNDARY_KINDS.has(value as BoundaryCaseKind);
114
+ }
115
+ }
@@ -38,7 +38,7 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
38
38
  const layerData = this.config.layers?.[layer] ?? {};
39
39
 
40
40
  const defaultValidators: Record<string, string[]> = {
41
- L2: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014'],
41
+ L2: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'],
42
42
  L3: ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
43
43
  L4: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'],
44
44
  };
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer infrastructure
3
3
  * @unit validator-system
4
+ * @work-item-id WI-117, WI-118
4
5
  *
5
6
  * MarkdownDesignDocumentAdapter — DesignDocumentPort実装
6
7
  */