phasegate 0.134.0 → 0.136.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 (41) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.ja.md +4 -4
  3. package/README.md +4 -3
  4. package/bin/phasegate +2 -0
  5. package/docs/contracts/lesson-artifact.schema.json +62 -0
  6. package/docs/contracts/requirement-test-matrix.schema.json +62 -0
  7. package/docs/guide/layer-model.md +2 -2
  8. package/docs/principles/architecture-philosophy.md +26 -55
  9. package/docs/principles/model-routing.md +30 -165
  10. package/docs/principles/testing-rules.md +66 -648
  11. package/docs/templates/ci/aidlc-gate.yml +97 -0
  12. package/docs/templates/ci/consistency-check.yml +117 -0
  13. package/docs/templates/hooks/commit-msg +13 -0
  14. package/docs/templates/hooks/pre-commit +62 -0
  15. package/package.json +5 -3
  16. package/scripts/harness/ci-governance/composition-root.ts +1 -1
  17. package/scripts/harness/ci-governance/infrastructure/adapters/yaml-template-renderer-adapter.ts +14 -31
  18. package/scripts/harness/ci-governance/presentation/handlers/generate-ci-template-handler.ts +19 -1
  19. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +2 -2
  20. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  21. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  22. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  23. package/scripts/harness/harness-error/infrastructure/adapters/validator-registry-bridge-adapter.ts +2 -0
  24. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +14 -0
  25. package/scripts/harness/main.ts +19 -7
  26. package/scripts/harness/phase2-extensions/composition-root.ts +7 -1
  27. package/scripts/harness/phase2-extensions/infrastructure/adapters/file-system-document-scanner-adapter.ts +16 -2
  28. package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +13 -2
  29. package/scripts/harness/phase2-extensions/infrastructure/adapters/regex-pointer-extractor-adapter.ts +35 -4
  30. package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-validator-id-registry-adapter.ts +1 -1
  31. package/scripts/harness/setup/skill-deployer.ts +48 -2
  32. package/scripts/harness/skill-quality/infrastructure/adapters/validator-id-registry-bridge-adapter.ts +1 -1
  33. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +3 -1
  34. package/scripts/harness/validator-system/application/dto/run-l4-validators-input.ts +1 -0
  35. package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +1 -0
  36. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +133 -3
  37. package/scripts/harness/validator-system/composition-root.ts +8 -2
  38. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +11 -4
  39. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +29 -10
  40. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +11 -2
  41. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +4 -0
@@ -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 {
@@ -420,10 +420,15 @@ export async function deployHookScripts(harnessRoot: string, projectRoot: string
420
420
  };
421
421
  }
422
422
 
423
+ export interface InitHarnessConfigOptions {
424
+ ciEnabled?: boolean;
425
+ }
426
+
423
427
  export async function initHarnessConfig(
424
428
  projectRoot: string,
425
429
  projectName: string,
426
430
  phasePreset?: "full" | "standard" | "minimal" | "custom",
431
+ options: InitHarnessConfigOptions = {},
427
432
  ): Promise<{ created: boolean; path: string }> {
428
433
  const configPath = join(projectRoot, HARNESS_CONFIG_FILE);
429
434
  try {
@@ -461,6 +466,7 @@ export async function initHarnessConfig(
461
466
  format: "json",
462
467
  outputDir: "reports",
463
468
  },
469
+ ...(options.ciEnabled ? { ci: { enabled: true } } : {}),
464
470
  };
465
471
 
466
472
  await fs.writeFile(configPath, JSON.stringify(template, null, 2) + "\n", "utf-8");
@@ -532,6 +538,46 @@ export interface DeployHuskyHookResult {
532
538
  path: string;
533
539
  }
534
540
 
541
+ export interface DeployCiWorkflowsResult {
542
+ copiedFiles: string[];
543
+ skippedFiles: string[];
544
+ }
545
+
546
+ export async function deployCiWorkflows(harnessRoot: string, projectRoot: string): Promise<DeployCiWorkflowsResult> {
547
+ const copiedFiles: string[] = [];
548
+ const skippedFiles: string[] = [];
549
+ const workflows = [
550
+ {
551
+ relativeSource: join("docs", "templates", "ci", "aidlc-gate.yml"),
552
+ relativeTarget: join(".github", "workflows", "aidlc-gate.yml"),
553
+ },
554
+ {
555
+ relativeSource: join("docs", "templates", "ci", "consistency-check.yml"),
556
+ relativeTarget: join(".github", "workflows", "consistency-check.yml"),
557
+ },
558
+ ];
559
+
560
+ await fs.mkdir(join(projectRoot, ".github", "workflows"), { recursive: true });
561
+
562
+ for (const workflow of workflows) {
563
+ const sourcePath = join(harnessRoot, workflow.relativeSource);
564
+ const targetPath = join(projectRoot, workflow.relativeTarget);
565
+
566
+ try {
567
+ await fs.access(targetPath);
568
+ skippedFiles.push(workflow.relativeTarget);
569
+ continue;
570
+ } catch {
571
+ // ファイルが存在しない場合のみコピーする
572
+ }
573
+
574
+ await fs.copyFile(sourcePath, targetPath);
575
+ copiedFiles.push(workflow.relativeTarget);
576
+ }
577
+
578
+ return { copiedFiles, skippedFiles };
579
+ }
580
+
535
581
  export async function deployHuskyHook(harnessRoot: string, projectRoot: string): Promise<DeployHuskyHookResult> {
536
582
  const targetPath = join(projectRoot, ".husky", "pre-commit");
537
583
 
@@ -540,7 +586,7 @@ export async function deployHuskyHook(harnessRoot: string, projectRoot: string):
540
586
  return { created: false, path: targetPath };
541
587
  } catch {}
542
588
 
543
- const sourcePath = join(harnessRoot, "templates", ".husky", "pre-commit");
589
+ const sourcePath = join(harnessRoot, "docs", "templates", "hooks", "pre-commit");
544
590
  await fs.mkdir(join(projectRoot, ".husky"), { recursive: true });
545
591
  await fs.copyFile(sourcePath, targetPath);
546
592
  await fs.chmod(targetPath, 0o755);
@@ -559,7 +605,7 @@ export async function deployHuskyCommitMsgHook(
559
605
  return { created: false, path: targetPath };
560
606
  } catch {}
561
607
 
562
- const sourcePath = join(harnessRoot, "templates", ".husky", "commit-msg");
608
+ const sourcePath = join(harnessRoot, "docs", "templates", "hooks", "commit-msg");
563
609
  await fs.mkdir(join(projectRoot, ".husky"), { recursive: true });
564
610
  await fs.copyFile(sourcePath, targetPath);
565
611
  await fs.chmod(targetPath, 0o755);
@@ -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 {
@@ -13,7 +13,8 @@ export type WorkItemStatus =
13
13
  | 'drafted'
14
14
  | 'reflected'
15
15
  | 'implemented'
16
- | 'tested';
16
+ | 'tested'
17
+ | 'completed';
17
18
 
18
19
  export interface WorkItemFrontmatter {
19
20
  readonly id: string;
@@ -54,4 +55,5 @@ export const WORK_ITEM_STATUSES: ReadonlySet<WorkItemStatus> = new Set([
54
55
  'reflected',
55
56
  'implemented',
56
57
  'tested',
58
+ 'completed',
57
59
  ]);
@@ -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));
@@ -12,9 +12,23 @@ import { basename, join, relative, sep } from 'node:path';
12
12
 
13
13
  const HARNESS_ROOT = join(process.cwd(), 'scripts', 'harness');
14
14
 
15
+ export interface BiomeAstSourceCodeAnalyzerAdapterOptions {
16
+ readonly sourceRoot?: string;
17
+ /** スキャン対象から除外するパスパターン(デフォルト: テストディレクトリを除外) */
18
+ readonly excludePattern?: RegExp;
19
+ }
20
+
15
21
  export class BiomeAstSourceCodeAnalyzerAdapter implements SourceCodeAnalyzerPort {
22
+ private readonly sourceRoot: string;
23
+ private readonly excludePattern: RegExp;
24
+
25
+ constructor(options: BiomeAstSourceCodeAnalyzerAdapterOptions = {}) {
26
+ this.sourceRoot = options.sourceRoot ?? HARNESS_ROOT;
27
+ this.excludePattern = options.excludePattern ?? /__tests__\//;
28
+ }
29
+
16
30
  async analyzeExports(targetUnits?: readonly string[]): Promise<readonly SourceAnalysisResult[]> {
17
- const targetFiles = await collectTargetFiles(targetUnits);
31
+ const targetFiles = await collectTargetFiles(this.sourceRoot, this.excludePattern, targetUnits);
18
32
  if (targetFiles.length === 0) return [];
19
33
 
20
34
  const program = ts.createProgram(targetFiles, {
@@ -30,7 +44,7 @@ export class BiomeAstSourceCodeAnalyzerAdapter implements SourceCodeAnalyzerPort
30
44
  const sourceFile = program.getSourceFile(filePath);
31
45
  if (!sourceFile) continue;
32
46
  results.push({
33
- unitName: resolveUnitName(filePath),
47
+ unitName: resolveUnitName(this.sourceRoot, filePath),
34
48
  filePath,
35
49
  exports: extractExports(sourceFile),
36
50
  imports: extractImports(sourceFile),
@@ -134,26 +148,31 @@ function extractImports(sourceFile: ts.SourceFile): SourceAnalysisResult['import
134
148
  return imports;
135
149
  }
136
150
 
137
- async function collectTargetFiles(targetUnits?: readonly string[]): Promise<readonly string[]> {
151
+ async function collectTargetFiles(
152
+ sourceRoot: string,
153
+ excludePattern: RegExp,
154
+ targetUnits?: readonly string[],
155
+ ): Promise<readonly string[]> {
138
156
  const roots = targetUnits && targetUnits.length > 0
139
- ? targetUnits.map((unit) => join(HARNESS_ROOT, unit))
140
- : [HARNESS_ROOT];
157
+ ? targetUnits.map((unit) => join(sourceRoot, unit))
158
+ : [sourceRoot];
141
159
 
142
160
  const files: string[] = [];
143
161
  for (const root of roots) {
144
- files.push(...await walkTsFiles(root));
162
+ files.push(...await walkTsFiles(root, excludePattern));
145
163
  }
146
164
 
147
165
  return files;
148
166
  }
149
167
 
150
- async function walkTsFiles(root: string): Promise<string[]> {
168
+ async function walkTsFiles(root: string, excludePattern: RegExp): Promise<string[]> {
151
169
  try {
152
170
  const entries = await readdir(root, { withFileTypes: true });
153
171
  const files = await Promise.all(entries.map(async (entry) => {
154
172
  const fullPath = join(root, entry.name);
173
+ if (excludePattern.test(fullPath)) return [];
155
174
  if (entry.isDirectory()) {
156
- return walkTsFiles(fullPath);
175
+ return walkTsFiles(fullPath, excludePattern);
157
176
  }
158
177
  return fullPath.endsWith('.ts') ? [fullPath] : [];
159
178
  }));
@@ -163,8 +182,8 @@ async function walkTsFiles(root: string): Promise<string[]> {
163
182
  }
164
183
  }
165
184
 
166
- function resolveUnitName(filePath: string): string {
167
- const relativePath = relative(HARNESS_ROOT, filePath);
185
+ function resolveUnitName(sourceRoot: string, filePath: string): string {
186
+ const relativePath = relative(sourceRoot, filePath);
168
187
  const [firstSegment] = relativePath.split(sep);
169
188
  return firstSegment || basename(filePath);
170
189
  }
@@ -6,6 +6,7 @@
6
6
  * HarnessConfigV2からLayerConfig VOを構築する
7
7
  */
8
8
  import { LayerConfig } from '../../domain/value-objects/layer-config.js';
9
+ import { ValidatorId } from '../../domain/value-objects/validator-id.js';
9
10
  import type { ValidatorConfigPort } from '../../domain/ports/validator-config-port.js';
10
11
 
11
12
  export interface HarnessConfigLayers {
@@ -39,7 +40,7 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
39
40
  const defaultValidators: Record<string, string[]> = {
40
41
  L2: ['L2-001', 'L2-002', 'L2-003'],
41
42
  L3: ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
42
- L4: ['L4-001', 'L4-002', 'L4-003'],
43
+ L4: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'],
43
44
  };
44
45
 
45
46
  const thresholds: Record<string, number> = {};
@@ -62,10 +63,18 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
62
63
  return LayerConfig.create({
63
64
  layer,
64
65
  enabled: layerData.enabled !== false,
65
- validatorIds: layerData.validators ?? defaultValidators[layer],
66
+ validatorIds: (layerData.validators ?? defaultValidators[layer]).map((idOrName) => this.normalizeValidatorId(idOrName)),
66
67
  thresholds,
67
68
  strictOnly: layerData.strictOnly ?? strictOnly,
68
69
  preset,
69
70
  });
70
71
  }
72
+
73
+ private normalizeValidatorId(idOrName: string): string {
74
+ try {
75
+ return ValidatorId.create(idOrName).value;
76
+ } catch {
77
+ return ValidatorId.fromName(idOrName).value;
78
+ }
79
+ }
71
80
  }
@@ -12,6 +12,10 @@ export class PhaseDependencyPhaseGatePolicyAdapter implements PhaseGatePolicyPor
12
12
  satisfied: boolean;
13
13
  violations: readonly HarnessErrorLike[];
14
14
  }> {
15
+ if (context.unitName.trim().length === 0) {
16
+ return { satisfied: true, violations: [] };
17
+ }
18
+
15
19
  try {
16
20
  // WI-085: paths config を phase-dependency-model に流入させる
17
21
  const { createConfigFoundationModule } = await import('../../../config-foundation/composition-root.js');