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.
- package/CHANGELOG.md +27 -0
- package/README.ja.md +4 -4
- package/README.md +4 -3
- 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/docs/templates/ci/aidlc-gate.yml +97 -0
- package/docs/templates/ci/consistency-check.yml +117 -0
- package/docs/templates/hooks/commit-msg +13 -0
- package/docs/templates/hooks/pre-commit +62 -0
- package/package.json +5 -3
- package/scripts/harness/ci-governance/composition-root.ts +1 -1
- package/scripts/harness/ci-governance/infrastructure/adapters/yaml-template-renderer-adapter.ts +14 -31
- package/scripts/harness/ci-governance/presentation/handlers/generate-ci-template-handler.ts +19 -1
- 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/main.ts +19 -7
- 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/setup/skill-deployer.ts +48 -2
- package/scripts/harness/skill-quality/infrastructure/adapters/validator-id-registry-bridge-adapter.ts +1 -1
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +3 -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/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:
|
|
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 {
|
|
@@ -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", "
|
|
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", "
|
|
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
|
]);
|
|
@@ -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));
|
|
@@ -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(
|
|
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(
|
|
140
|
-
: [
|
|
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(
|
|
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');
|