phasegate 0.134.0 → 0.135.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 (28) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bin/phasegate +2 -0
  3. package/docs/contracts/lesson-artifact.schema.json +62 -0
  4. package/docs/contracts/requirement-test-matrix.schema.json +62 -0
  5. package/docs/guide/layer-model.md +2 -2
  6. package/docs/principles/architecture-philosophy.md +26 -55
  7. package/docs/principles/model-routing.md +30 -165
  8. package/docs/principles/testing-rules.md +66 -648
  9. package/package.json +4 -3
  10. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +2 -2
  11. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  12. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  13. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  14. package/scripts/harness/harness-error/infrastructure/adapters/validator-registry-bridge-adapter.ts +2 -0
  15. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +14 -0
  16. package/scripts/harness/phase2-extensions/composition-root.ts +7 -1
  17. package/scripts/harness/phase2-extensions/infrastructure/adapters/file-system-document-scanner-adapter.ts +16 -2
  18. package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +13 -2
  19. package/scripts/harness/phase2-extensions/infrastructure/adapters/regex-pointer-extractor-adapter.ts +35 -4
  20. package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-validator-id-registry-adapter.ts +1 -1
  21. package/scripts/harness/skill-quality/infrastructure/adapters/validator-id-registry-bridge-adapter.ts +1 -1
  22. package/scripts/harness/validator-system/application/dto/run-l4-validators-input.ts +1 -0
  23. package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +1 -0
  24. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +133 -3
  25. package/scripts/harness/validator-system/composition-root.ts +8 -2
  26. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +11 -4
  27. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +29 -10
  28. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +11 -2
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.134.0",
3
+ "version": "0.135.0",
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",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/junpei-9898/phasegate.git"
9
+ "url": "git+https://github.com/junpei-9898/phasegate.git"
10
10
  },
11
11
  "homepage": "https://github.com/junpei-9898/phasegate#readme",
12
12
  "bugs": {
@@ -35,6 +35,7 @@
35
35
  "skills/**",
36
36
  "templates/**",
37
37
  "docs/ADR/**",
38
+ "docs/contracts/**",
38
39
  "docs/principles/**",
39
40
  "docs/guide/**",
40
41
  "docs/folder_management_rules.md",
@@ -42,7 +43,7 @@
42
43
  "CHANGELOG.md"
43
44
  ],
44
45
  "bin": {
45
- "phasegate": "./bin/phasegate"
46
+ "phasegate": "bin/phasegate"
46
47
  },
47
48
  "scripts": {
48
49
  "phasegate": "npx tsx scripts/harness/main.ts",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit config-foundation
4
- * @work-item-id WI-092 / WI-094
4
+ * @work-item-id WI-092 / WI-094 / WI-033
5
5
  */
6
6
  import type { HarnessConfigV2 } from '../../domain/harness-config.js';
7
7
 
@@ -13,7 +13,7 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
13
13
  layers: {
14
14
  L2: { enabled: resolvedConfig.layers.L2.enabled },
15
15
  L3: { enabled: resolvedConfig.layers.L3.enabled },
16
- L4: { enabled: resolvedConfig.layers.L4.enabled },
16
+ L4: { enabled: resolvedConfig.layers.L4.enabled, validators: resolvedConfig.layers.L4.validators },
17
17
  },
18
18
  validate: {
19
19
  failOnWarning: resolvedConfig.validate.failOnWarning,
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "L4": {
17
17
  "enabled": false,
18
- "validators": ["drift-detector"],
18
+ "validators": ["drift-detector", "doc-freshness-checker", "pointer-validator"],
19
19
  "schedule": "0 0 * * *"
20
20
  }
21
21
  },
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "L4": {
17
17
  "enabled": false,
18
- "validators": ["drift-detector"],
18
+ "validators": ["drift-detector", "doc-freshness-checker", "pointer-validator"],
19
19
  "schedule": "0 0 * * *"
20
20
  }
21
21
  },
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "L4": {
17
17
  "enabled": true,
18
- "validators": ["drift-detector", "dead-code-detector"],
18
+ "validators": ["drift-detector", "consistency-checker", "dead-code-detector", "doc-freshness-checker", "pointer-validator"],
19
19
  "schedule": "0 1 * * *"
20
20
  }
21
21
  },
@@ -64,6 +64,8 @@ const DEFAULT_VALIDATOR_IDS = [
64
64
  'drift-detect',
65
65
  'consistency-check',
66
66
  'dead-code',
67
+ 'doc-freshness',
68
+ 'pointer-validation',
67
69
  ] as const;
68
70
 
69
71
  export const DEFAULT_VALIDATOR_ENTRYPOINTS = new Map<
@@ -50,4 +50,18 @@ export const L4_ERROR_DEFINITIONS = Object.freeze([
50
50
  ownerValidatorId: 'dead-code',
51
51
  defaultFixExample: 'const actual = "remove unused export";',
52
52
  }),
53
+ createDefinition({
54
+ code: 'L4-004',
55
+ title: '設計ドキュメントの鮮度が閾値を超過した',
56
+ category: 'consistency',
57
+ ownerValidatorId: 'doc-freshness',
58
+ defaultFixExample: 'const actual = "review or refresh stale design document";',
59
+ }),
60
+ createDefinition({
61
+ code: 'L4-005',
62
+ title: '設計ドキュメント内のポインタ参照が解決できない',
63
+ category: 'consistency',
64
+ ownerValidatorId: 'pointer-validation',
65
+ defaultFixExample: 'const actual = "fix unresolved document pointer";',
66
+ }),
53
67
  ]);
@@ -25,7 +25,13 @@ import { ValidatePointersHandler } from './presentation/handlers/validate-pointe
25
25
 
26
26
  export function buildPhase2Extensions(projectRoot: string, config?: HarnessConfigV2) {
27
27
  const configAdapter = new HarnessConfigFreshnessAdapter(config);
28
- const documentScanner = new FileSystemDocumentScannerAdapter(projectRoot);
28
+ const inceptionDocsRoot = config?.paths?.inceptionDocs.replace(/\\/g, '/').replace(/\/+$/g, '') ?? 'docs/inception';
29
+ const documentScanner = new FileSystemDocumentScannerAdapter(projectRoot, {
30
+ excludePatterns: [
31
+ new RegExp(`^${inceptionDocsRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`),
32
+ /^docs\/.*\/archive\//,
33
+ ],
34
+ });
29
35
  const documentAge = new GitLogDocumentAgeAdapter(projectRoot);
30
36
  const pointerExtractor = new RegexPointerExtractorAdapter(projectRoot);
31
37
  const pointerResolver = new FileSystemPointerResolverAdapter(projectRoot);
@@ -6,6 +6,10 @@ import * as fs from 'node:fs/promises';
6
6
  import * as path from 'node:path';
7
7
  import type { DocumentScannerPort } from '../../domain/ports/document-scanner-port.js';
8
8
 
9
+ export interface FileSystemDocumentScannerAdapterOptions {
10
+ readonly excludePatterns?: readonly RegExp[];
11
+ }
12
+
9
13
  function escapeRegex(value: string): string {
10
14
  return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
11
15
  }
@@ -67,11 +71,21 @@ async function walk(root: string, current = ''): Promise<string[]> {
67
71
  }
68
72
 
69
73
  export class FileSystemDocumentScannerAdapter implements DocumentScannerPort {
70
- constructor(private readonly projectRoot: string) {}
74
+ private readonly excludePatterns: readonly RegExp[];
75
+
76
+ constructor(
77
+ private readonly projectRoot: string,
78
+ options: FileSystemDocumentScannerAdapterOptions = {},
79
+ ) {
80
+ this.excludePatterns = options.excludePatterns ?? [];
81
+ }
71
82
 
72
83
  async scan(pattern: string): Promise<string[]> {
73
84
  const files = await walk(this.projectRoot);
74
85
  const regex = toPatternRegex(pattern);
75
- return files.filter((file) => regex.test(file)).sort();
86
+ return files
87
+ .filter((file) => regex.test(file))
88
+ .filter((file) => !this.excludePatterns.some((excludePattern) => excludePattern.test(file)))
89
+ .sort();
76
90
  }
77
91
  }
@@ -9,6 +9,10 @@ import type { FreshnessConfigPort } from '../../domain/ports/freshness-config-po
9
9
  import { FreshnessThreshold } from '../../domain/value-objects/freshness-threshold.js';
10
10
 
11
11
  type Phase2RuleConfig = {
12
+ paths?: {
13
+ designDocs?: string;
14
+ inceptionDocs?: string;
15
+ };
12
16
  phase2Extensions?: {
13
17
  freshnessRules?: Array<{
14
18
  ruleId: string;
@@ -25,16 +29,22 @@ type Phase2RuleConfig = {
25
29
  };
26
30
  };
27
31
 
32
+ function normalizePathPatternRoot(value: string | undefined): string {
33
+ const normalized = value?.replace(/\\/g, '/').replace(/\/+$/g, '');
34
+ return normalized && normalized.length > 0 ? normalized : 'docs/product/construction';
35
+ }
36
+
28
37
  export class HarnessConfigFreshnessAdapter implements FreshnessConfigPort {
29
38
  constructor(private readonly config?: HarnessConfigV2 | Phase2RuleConfig) {}
30
39
 
31
40
  async loadRules(): Promise<DocFreshnessRule[]> {
32
41
  const configRules = this.config && 'phase2Extensions' in this.config ? this.config.phase2Extensions?.freshnessRules : undefined;
33
42
  if (!configRules || configRules.length === 0) {
43
+ const designDocsRoot = normalizePathPatternRoot(this.config?.paths?.designDocs);
34
44
  return [
35
45
  DocFreshnessRule.create({
36
46
  ruleId: 'default-doc-freshness',
37
- documentPattern: 'docs/**/*.md',
47
+ documentPattern: `${designDocsRoot}/**/*.md`,
38
48
  threshold: FreshnessThreshold.create({ warnThresholdDays: 30, errorThresholdDays: 90 }),
39
49
  enabled: true,
40
50
  }),
@@ -57,10 +67,11 @@ export class HarnessConfigFreshnessAdapter implements FreshnessConfigPort {
57
67
  async loadPointerRules(): Promise<PointerRule[]> {
58
68
  const configRules = this.config && 'phase2Extensions' in this.config ? this.config.phase2Extensions?.pointerRules : undefined;
59
69
  if (!configRules || configRules.length === 0) {
70
+ const designDocsRoot = normalizePathPatternRoot(this.config?.paths?.designDocs);
60
71
  return [
61
72
  PointerRule.create({
62
73
  ruleId: 'default-pointer-rule',
63
- documentPattern: 'docs/**/*.md',
74
+ documentPattern: `${designDocsRoot}/**/*.md`,
64
75
  failOnBroken: true,
65
76
  }),
66
77
  ];
@@ -8,12 +8,38 @@ import type { PointerExtractorPort } from '../../domain/ports/pointer-extractor-
8
8
  import { Pointer } from '../../domain/value-objects/pointer.js';
9
9
 
10
10
  const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
11
- const RELATIVE_PATH_REGEX = /(?:^|\s)((?:docs|scripts)\/[^\s,'")\]]+)/gm;
11
+ const RELATIVE_PATH_REGEX = /(?:^|\s)((?:docs|scripts)\/[^\s,'"`")\]]+)/gm;
12
12
 
13
13
  function isUrlTarget(target: string): boolean {
14
14
  return target.startsWith('http://') || target.startsWith('https://');
15
15
  }
16
16
 
17
+ function hasFileExtension(target: string): boolean {
18
+ return /\.[A-Za-z0-9]+$/u.test(path.posix.basename(target));
19
+ }
20
+
21
+ function normalizeFileTarget(documentPath: string, rawTarget: string): string | null {
22
+ const withoutFragment = rawTarget.split('#')[0] ?? '';
23
+ const cleaned = withoutFragment
24
+ .replace(/^`+|`+$/g, '')
25
+ .replace(/(.*$/u, '')
26
+ .replace(/:\d+$/u, '')
27
+ .replace(/[.,;:]+$/g, '')
28
+ .trim();
29
+
30
+ if (cleaned.length === 0) return null;
31
+ if (cleaned.includes('{') || cleaned.includes('}') || cleaned.includes('*') || cleaned.includes('...')) return null;
32
+ if (/[^\x00-\x7F]/u.test(cleaned)) return null;
33
+ if (cleaned.startsWith('/')) return path.posix.normalize(cleaned);
34
+ if (cleaned.startsWith('docs/') || cleaned.startsWith('scripts/')) return path.posix.normalize(cleaned);
35
+ if (!cleaned.includes('/') && !hasFileExtension(cleaned)) return null;
36
+ if (cleaned.startsWith('./') || cleaned.startsWith('../') || !cleaned.includes('/')) {
37
+ return path.posix.normalize(path.posix.join(path.posix.dirname(documentPath), cleaned));
38
+ }
39
+
40
+ return path.posix.normalize(cleaned);
41
+ }
42
+
17
43
  export class RegexPointerExtractorAdapter implements PointerExtractorPort {
18
44
  constructor(private readonly projectRoot: string) {}
19
45
 
@@ -29,15 +55,20 @@ export class RegexPointerExtractorAdapter implements PointerExtractorPort {
29
55
  continue;
30
56
  }
31
57
  const type = isUrlTarget(target) ? 'url' : 'file-path';
32
- const key = `${type}:${target}`;
58
+ const normalizedTarget = type === 'url' ? target : normalizeFileTarget(documentPath, target);
59
+ if (!normalizedTarget) {
60
+ continue;
61
+ }
62
+ const key = `${type}:${normalizedTarget}`;
33
63
  if (!seen.has(key)) {
34
64
  seen.add(key);
35
- pointers.push(Pointer.create({ type, rawText, target }));
65
+ pointers.push(Pointer.create({ type, rawText, target: normalizedTarget }));
36
66
  }
37
67
  }
38
68
 
39
69
  for (const match of content.matchAll(RELATIVE_PATH_REGEX)) {
40
- const target = match[1]?.trim();
70
+ const rawTarget = match[1]?.trim();
71
+ const target = rawTarget ? normalizeFileTarget(documentPath, rawTarget) : null;
41
72
  if (!target) {
42
73
  continue;
43
74
  }
@@ -9,7 +9,7 @@ const STATIC_VALIDATOR_IDS: readonly string[] = Object.freeze([
9
9
  'L1-001', 'L1-002', 'L1-003', 'L1-004', 'L1-005', 'L1-006', 'L1-007', 'L1-008',
10
10
  'L2-001', 'L2-002', 'L2-003',
11
11
  'L3-001', 'L3-002', 'L3-003', 'L3-004',
12
- 'L4-001', 'L4-002', 'L4-003',
12
+ 'L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005',
13
13
  ]);
14
14
 
15
15
  export class ValidatorSystemValidatorIdRegistryAdapter {
@@ -8,7 +8,7 @@ const FALLBACK_VALIDATOR_IDS = [
8
8
  'L1-001', 'L1-002', 'L1-003', 'L1-004', 'L1-005', 'L1-006', 'L1-007', 'L1-008',
9
9
  'L2-001', 'L2-002', 'L2-003',
10
10
  'L3-001', 'L3-002', 'L3-003', 'L3-004',
11
- 'L4-001', 'L4-002', 'L4-003',
11
+ 'L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005',
12
12
  ];
13
13
 
14
14
  export class ValidatorIdRegistryBridgeAdapter implements ValidatorIdRegistryPort {
@@ -8,4 +8,5 @@ export interface RunL4ValidatorsInput {
8
8
  readonly validatorIds?: readonly string[];
9
9
  readonly targetUnits?: readonly string[];
10
10
  readonly strictMode?: boolean;
11
+ readonly forceLayerEnabled?: boolean;
11
12
  }
@@ -78,6 +78,7 @@ export class RunFullValidationUseCase {
78
78
  if (runL4) {
79
79
  l4Results = await this.l4UseCase.execute({
80
80
  targetUnits: input.targetUnits,
81
+ forceLayerEnabled: input.targetLayers?.length === 1 && input.targetLayers.includes('L4'),
81
82
  });
82
83
  }
83
84
 
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { ValidatorId } from '../../domain/value-objects/validator-id.js';
8
8
  import { ValidationResult } from '../../domain/value-objects/validation-result.js';
9
+ import { LayerConfig } from '../../domain/value-objects/layer-config.js';
9
10
  import { ValidatorRegistry } from '../../domain/services/validator-registry.js';
10
11
  import { ValidatorExecutionService, ValidatorExecutionError } from '../../domain/services/validator-execution-service.js';
11
12
  import { ValidationResultContractMapper } from '../mappers/validation-result-contract-mapper.js';
@@ -16,6 +17,38 @@ import type { DriftDetectionService } from '../../domain/services/l4/drift-detec
16
17
  import type { ConsistencyCheckService } from '../../domain/services/l4/consistency-check-service.js';
17
18
  import type { DeadCodeDetectionService } from '../../domain/services/l4/dead-code-detection-service.js';
18
19
 
20
+ interface ScheduledHarnessErrorContract {
21
+ readonly severity: string;
22
+ readonly message: string;
23
+ readonly suggestion: string;
24
+ }
25
+
26
+ interface CheckDocFreshnessOutputContract {
27
+ readonly results: readonly {
28
+ readonly level: 'ok' | 'warn' | 'error';
29
+ readonly message: string;
30
+ }[];
31
+ readonly errors: readonly ScheduledHarnessErrorContract[];
32
+ }
33
+
34
+ interface ValidateDocPointersOutputContract {
35
+ readonly results: readonly {
36
+ readonly documentPath: string;
37
+ readonly pointerTarget: string;
38
+ readonly isResolvable: boolean;
39
+ readonly errorMessage: string | null;
40
+ }[];
41
+ readonly errors: readonly ScheduledHarnessErrorContract[];
42
+ }
43
+
44
+ interface CheckDocFreshnessUseCasePort {
45
+ execute(input: { targetPattern?: string; format?: 'text' | 'json'; dryRun?: boolean }): Promise<CheckDocFreshnessOutputContract>;
46
+ }
47
+
48
+ interface ValidateDocPointersUseCasePort {
49
+ execute(input: { targetPattern?: string; includeUrlPointers?: boolean; format?: 'text' | 'json' }): Promise<ValidateDocPointersOutputContract>;
50
+ }
51
+
19
52
  export class DesignDocumentReadError extends Error {
20
53
  constructor(message: string) {
21
54
  super(message);
@@ -31,6 +64,8 @@ export interface RunL4ValidatorsUseCaseDeps {
31
64
  driftDetectionService?: DriftDetectionService;
32
65
  consistencyCheckService?: ConsistencyCheckService;
33
66
  deadCodeDetectionService?: DeadCodeDetectionService;
67
+ checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
68
+ validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
34
69
  }
35
70
 
36
71
  export class RunL4ValidatorsUseCase {
@@ -41,6 +76,8 @@ export class RunL4ValidatorsUseCase {
41
76
  private readonly driftDetectionService?: DriftDetectionService;
42
77
  private readonly consistencyCheckService?: ConsistencyCheckService;
43
78
  private readonly deadCodeDetectionService?: DeadCodeDetectionService;
79
+ private readonly checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
80
+ private readonly validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
44
81
 
45
82
  constructor(deps: RunL4ValidatorsUseCaseDeps) {
46
83
  this.registry = deps.validatorRegistry;
@@ -50,6 +87,8 @@ export class RunL4ValidatorsUseCase {
50
87
  this.driftDetectionService = deps.driftDetectionService;
51
88
  this.consistencyCheckService = deps.consistencyCheckService;
52
89
  this.deadCodeDetectionService = deps.deadCodeDetectionService;
90
+ this.checkDocFreshnessUseCase = deps.checkDocFreshnessUseCase;
91
+ this.validateDocPointersUseCase = deps.validateDocPointersUseCase;
53
92
  }
54
93
 
55
94
  async execute(input: RunL4ValidatorsInput): Promise<readonly ValidationResultContract[]> {
@@ -69,11 +108,22 @@ export class RunL4ValidatorsUseCase {
69
108
  throw new ValidatorExecutionError(`Failed to get L4 LayerConfig: ${err instanceof Error ? err.message : String(err)}`, err);
70
109
  }
71
110
 
72
- if (!layerConfig.enabled) {
111
+ if (!layerConfig.enabled && !input.forceLayerEnabled) {
73
112
  return [];
74
113
  }
75
114
 
76
- const results = this.executionService.execute(definitions, [layerConfig]);
115
+ const effectiveLayerConfig = input.forceLayerEnabled && !layerConfig.enabled
116
+ ? LayerConfig.create({
117
+ layer: layerConfig.layer,
118
+ enabled: true,
119
+ validatorIds: layerConfig.validatorIds,
120
+ thresholds: { ...layerConfig.thresholds },
121
+ strictOnly: layerConfig.strictOnly,
122
+ preset: layerConfig.preset,
123
+ })
124
+ : layerConfig;
125
+
126
+ const results = this.executionService.execute(definitions, [effectiveLayerConfig]);
77
127
  const overrideMap = new Map<string, ValidationResult>(results.map((result) => [result.validatorId.value, result]));
78
128
 
79
129
  if (this.driftDetectionService) {
@@ -109,7 +159,7 @@ export class RunL4ValidatorsUseCase {
109
159
  if (this.deadCodeDetectionService) {
110
160
  const l4003Result = overrideMap.get('L4-003');
111
161
  if (l4003Result && !l4003Result.skipped) {
112
- const strictOnly = input.strictMode ?? layerConfig.strictOnly ?? false;
162
+ const strictOnly = input.strictMode ?? effectiveLayerConfig.strictOnly ?? false;
113
163
  const report = await this.deadCodeDetectionService.detect({ strictOnly });
114
164
  if (report.hasDeadCode()) {
115
165
  overrideMap.set(
@@ -120,9 +170,89 @@ export class RunL4ValidatorsUseCase {
120
170
  }
121
171
  }
122
172
 
173
+ if (this.checkDocFreshnessUseCase) {
174
+ const l4004Result = overrideMap.get('L4-004');
175
+ if (l4004Result && !l4004Result.skipped) {
176
+ const freshnessOutput = await this.checkDocFreshnessUseCase.execute({ format: 'json' });
177
+ const errors = this.toDocFreshnessHarnessErrors(freshnessOutput);
178
+ overrideMap.set(
179
+ 'L4-004',
180
+ errors.length > 0
181
+ ? ValidationResult.fail(ValidatorId.create('L4-004'), errors, 0)
182
+ : ValidationResult.pass(ValidatorId.create('L4-004'), 0),
183
+ );
184
+ }
185
+ }
186
+
187
+ if (this.validateDocPointersUseCase) {
188
+ const l4005Result = overrideMap.get('L4-005');
189
+ if (l4005Result && !l4005Result.skipped) {
190
+ const pointerOutput = await this.validateDocPointersUseCase.execute({ includeUrlPointers: false, format: 'json' });
191
+ const errors = this.toPointerValidationHarnessErrors(pointerOutput);
192
+ overrideMap.set(
193
+ 'L4-005',
194
+ errors.length > 0
195
+ ? ValidationResult.fail(ValidatorId.create('L4-005'), errors, 0)
196
+ : ValidationResult.pass(ValidatorId.create('L4-005'), 0),
197
+ );
198
+ }
199
+ }
200
+
123
201
  const finalResults = definitions.map(
124
202
  (definition) => overrideMap.get(definition.validatorId.value) ?? ValidationResult.skip(definition.validatorId),
125
203
  );
126
204
  return this.mapper.toContracts(finalResults);
127
205
  }
206
+
207
+ private toDocFreshnessHarnessErrors(output: CheckDocFreshnessOutputContract): readonly ValidationResult['errors'][number][] {
208
+ const executionErrors = output.errors.map((error) => this.toHarnessErrorLike(
209
+ 'L4-004',
210
+ error.severity,
211
+ error.message,
212
+ error.suggestion,
213
+ ));
214
+ const freshnessFindings = output.results
215
+ .filter((result) => result.level !== 'ok')
216
+ .map((result) => this.toHarnessErrorLike(
217
+ 'L4-004',
218
+ result.level === 'error' ? 'error' : 'warning',
219
+ result.message,
220
+ 'Review the document freshness threshold or update the design document.',
221
+ ));
222
+
223
+ return [...executionErrors, ...freshnessFindings];
224
+ }
225
+
226
+ private toPointerValidationHarnessErrors(output: ValidateDocPointersOutputContract): readonly ValidationResult['errors'][number][] {
227
+ const executionErrors = output.errors.map((error) => this.toHarnessErrorLike(
228
+ 'L4-005',
229
+ error.severity,
230
+ error.message,
231
+ error.suggestion,
232
+ ));
233
+ const brokenPointers = output.results
234
+ .filter((result) => !result.isResolvable)
235
+ .map((result) => this.toHarnessErrorLike(
236
+ 'L4-005',
237
+ 'warning',
238
+ `${result.documentPath} has an unresolved pointer to ${result.pointerTarget}`,
239
+ result.errorMessage ?? 'Fix or remove the pointer target.',
240
+ ));
241
+
242
+ return [...executionErrors, ...brokenPointers];
243
+ }
244
+
245
+ private toHarnessErrorLike(
246
+ code: string,
247
+ severity: string,
248
+ message: string,
249
+ suggestion: string,
250
+ ): ValidationResult['errors'][number] {
251
+ return {
252
+ code: { value: code, toString: () => code },
253
+ severity: { value: severity, toString: () => severity },
254
+ message,
255
+ suggestion,
256
+ };
257
+ }
128
258
  }
@@ -35,6 +35,7 @@ import { ImportGraphSourceAnalysisAdapter } from './infrastructure/adapters/impo
35
35
  import { DriftDetectionService } from './domain/services/l4/drift-detection-service.js';
36
36
  import { ConsistencyCheckService } from './domain/services/l4/consistency-check-service.js';
37
37
  import { DeadCodeDetectionService } from './domain/services/l4/dead-code-detection-service.js';
38
+ import { buildPhase2Extensions } from '../phase2-extensions/composition-root.js';
38
39
  import { RunValidatorsHandler } from './presentation/handlers/run-validators-handler.js';
39
40
  import { RunQuickModeHandler } from './presentation/handlers/run-quick-mode-handler.js';
40
41
  import { ReportValidationResultsHandler } from './presentation/handlers/report-validation-results-handler.js';
@@ -46,12 +47,12 @@ const DEFAULT_CONFIG = {
46
47
  layers: {
47
48
  L2: { enabled: true, validators: ['L2-001', 'L2-002', 'L2-003'] },
48
49
  L3: { enabled: true, validators: ['L3-001', 'L3-002', 'L3-003', 'L3-004'], coverageThreshold: 90, bundleSizeLimit: 512000 },
49
- L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003'] },
50
+ L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'] },
50
51
  },
51
52
  validate: { failOnWarning: false },
52
53
  };
53
54
 
54
- /** バリデータ定義カタログ(全10件) */
55
+ /** バリデータ定義カタログ */
55
56
  function buildDefaultRegistry(): ValidatorRegistry {
56
57
  const defaultRule = ValidationRule.create({
57
58
  ruleName: 'default-rule',
@@ -83,6 +84,8 @@ function buildDefaultRegistry(): ValidatorRegistry {
83
84
  createDef('L4-001', 'L4', 'always'),
84
85
  createDef('L4-002', 'L4', 'always'),
85
86
  createDef('L4-003', 'L4', 'strictOnly'),
87
+ createDef('L4-004', 'L4', 'always'),
88
+ createDef('L4-005', 'L4', 'always'),
86
89
  ];
87
90
 
88
91
  return new ValidatorRegistry(definitions);
@@ -157,6 +160,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
157
160
  const deadCodeDetectionService = new DeadCodeDetectionService({
158
161
  sourceAnalysisPort,
159
162
  });
163
+ const phase2Extensions = buildPhase2Extensions(process.cwd(), configData as never);
160
164
 
161
165
  const runL4ValidatorsUseCase = new RunL4ValidatorsUseCase({
162
166
  validatorRegistry: registry,
@@ -166,6 +170,8 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
166
170
  driftDetectionService,
167
171
  consistencyCheckService,
168
172
  deadCodeDetectionService,
173
+ checkDocFreshnessUseCase: phase2Extensions.checkDocFreshnessUseCase,
174
+ validateDocPointersUseCase: phase2Extensions.validateDocPointersUseCase,
169
175
  });
170
176
 
171
177
  const runQuickModeUseCase = new RunQuickModeUseCase({
@@ -3,7 +3,7 @@
3
3
  * @unit validator-system
4
4
  *
5
5
  * ValidatorId 値オブジェクト
6
- * L1-001〜L4-003 のバリデータを識別する不変値オブジェクト
6
+ * L1-001〜L4-005 のバリデータを識別する不変値オブジェクト
7
7
  * Wave 2A で L1-017, L1-018, L2-013 を追加
8
8
  */
9
9
 
@@ -33,12 +33,19 @@ const VALIDATOR_NAME_MAP: Record<string, string> = {
33
33
  'L4-001': 'drift-detect',
34
34
  'L4-002': 'consistency-check',
35
35
  'L4-003': 'dead-code',
36
+ 'L4-004': 'doc-freshness',
37
+ 'L4-005': 'pointer-validation',
36
38
  };
37
39
 
38
40
  /** バリデータ名 -> バリデータID の逆引きマップ */
39
- const NAME_TO_ID_MAP: Record<string, string> = Object.fromEntries(
40
- Object.entries(VALIDATOR_NAME_MAP).map(([id, name]) => [name, id])
41
- );
41
+ const NAME_TO_ID_MAP: Record<string, string> = {
42
+ ...Object.fromEntries(Object.entries(VALIDATOR_NAME_MAP).map(([id, name]) => [name, id])),
43
+ 'drift-detector': 'L4-001',
44
+ 'consistency-checker': 'L4-002',
45
+ 'dead-code-detector': 'L4-003',
46
+ 'doc-freshness-checker': 'L4-004',
47
+ 'pointer-validator': 'L4-005',
48
+ };
42
49
 
43
50
  /** 有効なValidatorID集合 */
44
51
  const VALID_IDS = new Set(Object.keys(VALIDATOR_NAME_MAP));