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.
- package/CHANGELOG.md +11 -0
- package/bin/phasegate +2 -0
- package/docs/contracts/lesson-artifact.schema.json +62 -0
- package/docs/contracts/requirement-test-matrix.schema.json +62 -0
- package/docs/guide/layer-model.md +2 -2
- package/docs/principles/architecture-philosophy.md +26 -55
- package/docs/principles/model-routing.md +30 -165
- package/docs/principles/testing-rules.md +66 -648
- package/package.json +4 -3
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
- package/scripts/harness/harness-error/infrastructure/adapters/validator-registry-bridge-adapter.ts +2 -0
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +14 -0
- package/scripts/harness/phase2-extensions/composition-root.ts +7 -1
- package/scripts/harness/phase2-extensions/infrastructure/adapters/file-system-document-scanner-adapter.ts +16 -2
- package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +13 -2
- package/scripts/harness/phase2-extensions/infrastructure/adapters/regex-pointer-extractor-adapter.ts +35 -4
- package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-validator-id-registry-adapter.ts +1 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/validator-id-registry-bridge-adapter.ts +1 -1
- package/scripts/harness/validator-system/application/dto/run-l4-validators-input.ts +1 -0
- package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +1 -0
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +133 -3
- package/scripts/harness/validator-system/composition-root.ts +8 -2
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +11 -4
- package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +29 -10
- 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.
|
|
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": "
|
|
46
|
+
"phasegate": "bin/phasegate"
|
|
46
47
|
},
|
|
47
48
|
"scripts": {
|
|
48
49
|
"phasegate": "npx tsx scripts/harness/main.ts",
|
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
CHANGED
|
@@ -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": 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
|
},
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
74
|
+
documentPattern: `${designDocsRoot}/**/*.md`,
|
|
64
75
|
failOnBroken: true,
|
|
65
76
|
}),
|
|
66
77
|
];
|
package/scripts/harness/phase2-extensions/infrastructure/adapters/regex-pointer-extractor-adapter.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
@@ -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
|
|
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 ??
|
|
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
|
-
/**
|
|
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-
|
|
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> =
|
|
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));
|