phasegate 0.133.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 (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.ja.md +1 -4
  3. package/README.md +1 -5
  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/configuration.md +0 -1
  8. package/docs/guide/layer-model.md +9 -9
  9. package/docs/principles/architecture-philosophy.md +26 -55
  10. package/docs/principles/model-routing.md +30 -165
  11. package/docs/principles/testing-rules.md +66 -648
  12. package/package.json +4 -3
  13. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +2 -2
  14. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  15. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  16. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  17. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +0 -16
  18. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +0 -16
  19. package/scripts/harness/harness-error/infrastructure/adapters/validator-registry-bridge-adapter.ts +2 -0
  20. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +14 -0
  21. package/scripts/harness/main.ts +3 -3
  22. package/scripts/harness/phase2-extensions/composition-root.ts +7 -1
  23. package/scripts/harness/phase2-extensions/infrastructure/adapters/file-system-document-scanner-adapter.ts +16 -2
  24. package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +13 -2
  25. package/scripts/harness/phase2-extensions/infrastructure/adapters/regex-pointer-extractor-adapter.ts +35 -4
  26. package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-validator-id-registry-adapter.ts +1 -1
  27. package/scripts/harness/skill-quality/infrastructure/adapters/validator-id-registry-bridge-adapter.ts +1 -1
  28. package/scripts/harness/validator-system/application/dto/run-l4-validators-input.ts +1 -0
  29. package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +1 -0
  30. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +133 -3
  31. package/scripts/harness/validator-system/composition-root.ts +9 -16
  32. package/scripts/harness/validator-system/domain/ports/validator-config-port.ts +1 -1
  33. package/scripts/harness/validator-system/domain/services/validator-execution-service.ts +2 -2
  34. package/scripts/harness/validator-system/domain/services/validator-registry.ts +2 -2
  35. package/scripts/harness/validator-system/domain/value-objects/layer-config.ts +2 -2
  36. package/scripts/harness/validator-system/domain/value-objects/validator-definition.ts +5 -6
  37. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +11 -6
  38. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +29 -10
  39. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +12 -5
  40. package/scripts/harness/validator-system/presentation/handlers/run-validators-handler.ts +6 -35
  41. package/scripts/harness/validator-system/application/dto/run-l0-validators-input.ts +0 -9
  42. package/scripts/harness/validator-system/application/use-cases/run-l0-validators-usecase.ts +0 -60
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.133.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
  },
@@ -68,22 +68,6 @@
68
68
  "type": "object",
69
69
  "additionalProperties": false,
70
70
  "properties": {
71
- "L0": {
72
- "type": "object",
73
- "additionalProperties": false,
74
- "properties": {
75
- "enabled": {
76
- "type": "boolean"
77
- },
78
- "validators": {
79
- "type": "array",
80
- "items": {
81
- "type": "string"
82
- },
83
- "uniqueItems": true
84
- }
85
- }
86
- },
87
71
  "L1": {
88
72
  "type": "object",
89
73
  "additionalProperties": false,
@@ -68,22 +68,6 @@
68
68
  "type": "object",
69
69
  "additionalProperties": false,
70
70
  "properties": {
71
- "L0": {
72
- "type": "object",
73
- "additionalProperties": false,
74
- "properties": {
75
- "enabled": {
76
- "type": "boolean"
77
- },
78
- "validators": {
79
- "type": "array",
80
- "items": {
81
- "type": "string"
82
- },
83
- "uniqueItems": true
84
- }
85
- }
86
- },
87
71
  "L1": {
88
72
  "type": "object",
89
73
  "additionalProperties": false,
@@ -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
  ]);
@@ -110,7 +110,7 @@ Commands:
110
110
 
111
111
  lint Run lint checks (--json, --target <path>)
112
112
 
113
- validate Run validators (--layer L0|L2|L3|L4|all, --unit, --format human|agent|ci)
113
+ validate Run validators (--layer L2|L3|L4|all; L0 prints runtime hook info, --unit, --format human|agent|ci)
114
114
  ci-check CI check (--quick for quick mode, --fail-on-reject, --dry-run, --files)
115
115
 
116
116
  phasegate:check-ready Check ready status (--json)
@@ -263,10 +263,10 @@ Options:
263
263
  --help, -h Show this help`,
264
264
  validate: `Usage: phasegate validate [options]
265
265
 
266
- Run validators against the project. Without --layer, runs all enabled layers (L0/L2/L3/L4).
266
+ Run validators against the project. Without --layer, runs all enabled validator layers (L2/L3/L4).
267
267
 
268
268
  Options:
269
- --layer <L0|L2|L3|L4> Run only the specified layer
269
+ --layer <L0|L2|L3|L4> Run only the specified validator layer; L0 prints runtime hook info
270
270
  --json Output machine-readable JSON
271
271
  --help, -h Show this help`,
272
272
  lint: `Usage: phasegate lint [options]
@@ -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
  }