phasegate 0.39.0 → 0.44.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/docs/guide/cli-reference.md +15 -0
- package/package.json +1 -1
- package/scripts/harness/ci-governance/application/usecases/generate-ci-template-usecase.ts +6 -2
- package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +11 -2
- package/scripts/harness/integrations/pre-commit.ts +64 -130
- package/scripts/harness/main.ts +42 -2
- package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-document-age-adapter.ts +7 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/git-diff-changed-files-adapter.ts +20 -1
- package/scripts/harness/regression-suite/composition-root.ts +4 -1
- package/scripts/harness/validator-system/application/dto/run-full-validation-input.ts +5 -0
- package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +40 -12
- package/scripts/harness/validator-system/composition-root.ts +2 -0
- package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +29 -2
- package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +17 -0
- package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts +65 -5
- package/scripts/harness/validator-system/presentation/handlers/run-validators-handler.ts +9 -0
|
@@ -66,6 +66,21 @@ Commands exposed as npm scripts (`npm run <command>`).
|
|
|
66
66
|
| `render-errors` | `--format human\|agent\|ci` | Render errors |
|
|
67
67
|
| `validate-fix` | `--code <code>` | Validate fix code example |
|
|
68
68
|
|
|
69
|
+
### `list-errors` と `render-errors` の使い分け
|
|
70
|
+
|
|
71
|
+
ISSUE-005 P3-10 で明確化された境界:
|
|
72
|
+
|
|
73
|
+
- **`list-errors`** — **定義駆動**。`HarnessError` Value Object の**静的な定義**を出力する。
|
|
74
|
+
コードを実行しないため、常に安定した結果を返す。`--format json` と組み合わせて**異なる
|
|
75
|
+
バージョン間の定義差分を比較**する用途に向く。
|
|
76
|
+
- **`render-errors`** — **ランタイム駆動**。実行時に蓄積されたエラーの**蓄積履歴**を整形する。
|
|
77
|
+
まだエラーが記録されていない環境では空を返すため、テストデータや実行痕跡を前提とする。
|
|
78
|
+
CI ログ向けの詳細フォーマット (`--format ci`) やエージェント送信向けの構造化 (`--format agent`)
|
|
79
|
+
に向く。
|
|
80
|
+
|
|
81
|
+
**差分比較を行いたい場合は `list-errors --format json`** を使い、`render-errors` は runtime 観測用と
|
|
82
|
+
位置付けること。
|
|
83
|
+
|
|
69
84
|
---
|
|
70
85
|
|
|
71
86
|
## Skill Quality
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ import type { TemplateGenerator } from '../../domain/services/template-generator
|
|
|
9
9
|
import type { GenerateCiTemplateInput } from '../dto/generate-ci-template-input.js';
|
|
10
10
|
import type { GenerateCiTemplateOutput } from '../dto/generate-ci-template-output.js';
|
|
11
11
|
import { CiTemplate } from '../../domain/aggregates/ci-template.js';
|
|
12
|
-
import { isTemplateType } from '../../domain/types/template-type.js';
|
|
12
|
+
import { isTemplateType, TEMPLATE_TYPES } from '../../domain/types/template-type.js';
|
|
13
13
|
|
|
14
14
|
export class GenerateCiTemplateUseCase {
|
|
15
15
|
constructor(private readonly templateGenerator: TemplateGenerator) {}
|
|
@@ -18,6 +18,10 @@ export class GenerateCiTemplateUseCase {
|
|
|
18
18
|
const { presetId, templateType } = input;
|
|
19
19
|
|
|
20
20
|
if (!isTemplateType(templateType)) {
|
|
21
|
+
const validValues = TEMPLATE_TYPES.map((t) => `'${t}'`).join(', ');
|
|
22
|
+
const hint = /github|gitlab|circle|jenkins|travis|actions/i.test(String(templateType))
|
|
23
|
+
? ` (Hint: --type specifies the template purpose, not the CI platform. Use one of: ${validValues})`
|
|
24
|
+
: ` (valid: ${validValues})`;
|
|
21
25
|
return {
|
|
22
26
|
templateType: templateType as any,
|
|
23
27
|
presetRef: presetId,
|
|
@@ -26,7 +30,7 @@ export class GenerateCiTemplateUseCase {
|
|
|
26
30
|
failOnWarning: false,
|
|
27
31
|
validationErrors: [{
|
|
28
32
|
code: 'CI_TEMPLATE_INVALID_TYPE',
|
|
29
|
-
message: `INV-1: Invalid templateType: ${templateType}`,
|
|
33
|
+
message: `INV-1: Invalid templateType: ${templateType}${hint}`,
|
|
30
34
|
}],
|
|
31
35
|
};
|
|
32
36
|
}
|
package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts
CHANGED
|
@@ -45,8 +45,17 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
|
|
|
45
45
|
},
|
|
46
46
|
|
|
47
47
|
async runDriftDetection(): Promise<DriftItem[]> {
|
|
48
|
-
//
|
|
49
|
-
|
|
48
|
+
// ISSUE-005 P1-5: L4-001 の DriftDetectionService を直接呼び出し、
|
|
49
|
+
// phasegate:detect-drift と validate --layer L4 の結果を一致させる
|
|
50
|
+
const { createValidatorSystemModule } = await import('../../../validator-system/composition-root.js');
|
|
51
|
+
const mod = createValidatorSystemModule();
|
|
52
|
+
const reports = await mod.driftDetectionService.detect();
|
|
53
|
+
return reports.map((r) => ({
|
|
54
|
+
direction: r.direction,
|
|
55
|
+
unit: r.unitName,
|
|
56
|
+
element: r.element,
|
|
57
|
+
recommendation: r.recommendation,
|
|
58
|
+
}));
|
|
50
59
|
},
|
|
51
60
|
};
|
|
52
61
|
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
// @layer infrastructure
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
2
|
+
* @unit harness-api
|
|
3
|
+
* @layer presentation
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Pre-commit CLI entry.
|
|
6
|
+
* Runs L2 validators (phase-gate / metadata / test-quality) against staged
|
|
7
|
+
* TypeScript files. Invoked from `.husky/pre-commit` or `npx phasegate pre-commit`.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* Exit codes:
|
|
10
|
+
* 0 = pass (or nothing to check)
|
|
11
|
+
* 1 = validation failure (commit blocked)
|
|
12
|
+
* 2 = runtime error
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
|
-
import { execSync } from
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import type {
|
|
16
|
-
import {
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { createValidatorSystemModule } from '../validator-system/composition-root.js';
|
|
17
|
+
import { HumanValidationResultFormatter } from '../validator-system/presentation/formatters/human-validation-result-formatter.js';
|
|
18
|
+
import type { AggregatedValidationReport } from '../validator-system/application/dto/aggregated-validation-report.js';
|
|
19
|
+
import type { ValidationResultContract } from '../validator-system/application/dto/validation-result-contract.js';
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const RED = "\x1b[31m";
|
|
24
|
-
const RESET = "\x1b[0m";
|
|
25
|
-
|
|
26
|
-
// ─── Staged File Detection ───
|
|
21
|
+
const GREEN = '\x1b[32m';
|
|
22
|
+
const RED = '\x1b[31m';
|
|
23
|
+
const BOLD = '\x1b[1m';
|
|
24
|
+
const DIM = '\x1b[2m';
|
|
25
|
+
const RESET = '\x1b[0m';
|
|
27
26
|
|
|
28
27
|
function getStagedFiles(): string[] {
|
|
29
28
|
try {
|
|
30
|
-
const output = execSync(
|
|
31
|
-
encoding:
|
|
29
|
+
const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
32
32
|
});
|
|
33
33
|
return output
|
|
34
|
-
.split(
|
|
34
|
+
.split('\n')
|
|
35
35
|
.map((f) => f.trim())
|
|
36
36
|
.filter((f) => f.length > 0);
|
|
37
37
|
} catch {
|
|
@@ -39,126 +39,60 @@ function getStagedFiles(): string[] {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{ name: "dependency", harnessKey: "dependency", category: "dependency", importPath: "../validators/dependency.js", exportName: "runDependencyCheck" },
|
|
64
|
-
{ name: "test-quality", harnessKey: "testQuality", category: "quality", importPath: "../validators/test-quality.js", exportName: "runTestQualityCheck" },
|
|
65
|
-
{ name: "security", harnessKey: "security", category: "security", importPath: "../validators/security.js", exportName: "runSecurityCheck" },
|
|
66
|
-
{ name: "performance", harnessKey: "performance", category: "performance", importPath: "../validators/performance.js", exportName: "runPerformanceCheck" },
|
|
67
|
-
{ name: "consistency", harnessKey: "consistency", category: "consistency", importPath: "../validators/consistency.js", exportName: "runConsistencyCheck" },
|
|
68
|
-
{ name: "metadata", harnessKey: "metadata", category: "quality", importPath: "../validators/metadata.js", exportName: "runMetadataCheck" },
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
async function loadValidator(entry: ValidatorEntry): Promise<ValidatorFn | null> {
|
|
72
|
-
try {
|
|
73
|
-
const mod = await import(entry.importPath);
|
|
74
|
-
return mod[entry.exportName] ?? null;
|
|
75
|
-
} catch {
|
|
76
|
-
// Validator not yet implemented, skip gracefully
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ─── Convert Validator Errors to HarnessErrors ───
|
|
82
|
-
|
|
83
|
-
function toHarnessErrors(
|
|
84
|
-
validatorErrors: Array<{ rule: string; severity: "error" | "warning"; file: string; message: string; suggestion?: string }>,
|
|
85
|
-
entry: ValidatorEntry,
|
|
86
|
-
): HarnessError[] {
|
|
87
|
-
return validatorErrors.map((err) =>
|
|
88
|
-
createError({
|
|
89
|
-
code: `HARNESS-${err.rule.toUpperCase().replace(/[^A-Z0-9]/g, "-")}`,
|
|
90
|
-
severity: err.severity,
|
|
91
|
-
category: entry.category,
|
|
92
|
-
location: { file: err.file },
|
|
93
|
-
message: {
|
|
94
|
-
short: err.message,
|
|
95
|
-
detailed: err.message,
|
|
96
|
-
agentInstruction: err.suggestion ?? "",
|
|
97
|
-
},
|
|
98
|
-
metadata: { timestamp: new Date().toISOString(), validator: entry.name, layer: "L2" },
|
|
99
|
-
}),
|
|
100
|
-
);
|
|
42
|
+
function buildReport(results: readonly ValidationResultContract[]): AggregatedValidationReport {
|
|
43
|
+
const passed = results.filter((r) => r.passed && !r.skipped).length;
|
|
44
|
+
const failed = results.filter((r) => !r.passed && !r.skipped).length;
|
|
45
|
+
const skipped = results.filter((r) => r.skipped).length;
|
|
46
|
+
const allErrors = results.flatMap((r) => r.errors);
|
|
47
|
+
const errorCount = allErrors.filter((e) => e.severity === 'error').length;
|
|
48
|
+
const warnCount = allErrors.filter((e) => e.severity === 'warning').length;
|
|
49
|
+
return {
|
|
50
|
+
overallPassed: failed === 0,
|
|
51
|
+
totalValidators: results.length,
|
|
52
|
+
passedValidators: passed,
|
|
53
|
+
failedValidators: failed,
|
|
54
|
+
skippedValidators: skipped,
|
|
55
|
+
allErrors,
|
|
56
|
+
summary: {
|
|
57
|
+
totalErrors: errorCount,
|
|
58
|
+
totalWarnings: warnCount,
|
|
59
|
+
errorsByLayer: { L2: errorCount, L3: 0, L4: 0 },
|
|
60
|
+
},
|
|
61
|
+
results,
|
|
62
|
+
};
|
|
101
63
|
}
|
|
102
64
|
|
|
103
|
-
// ─── Main ───
|
|
104
|
-
|
|
105
65
|
async function main(): Promise<void> {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
// Check if L2 (pre-commit) layer is enabled
|
|
109
|
-
if (!config.layers.L2_precommit.enabled) {
|
|
110
|
-
console.log(`${DIM}[harness] L2 Pre-commit layer is disabled. Skipping.${RESET}`);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Get staged .ts files, filtering excluded paths
|
|
115
|
-
const allStaged = getStagedFiles();
|
|
116
|
-
const tsFiles = allStaged
|
|
117
|
-
.filter((f) => f.endsWith(".ts"))
|
|
118
|
-
.filter((f) => !isExcludedPath(f, config));
|
|
66
|
+
const stagedFiles = getStagedFiles();
|
|
67
|
+
const tsFiles = stagedFiles.filter((f) => f.endsWith('.ts'));
|
|
119
68
|
|
|
120
69
|
if (tsFiles.length === 0) {
|
|
121
|
-
|
|
122
|
-
|
|
70
|
+
process.stdout.write(`${DIM}[phasegate] No staged TypeScript files. Skipping.${RESET}\n`);
|
|
71
|
+
process.exit(0);
|
|
123
72
|
}
|
|
124
73
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const allErrors: HarnessError[] = [];
|
|
74
|
+
process.stdout.write(`${BOLD}[phasegate]${RESET} Pre-commit check (${tsFiles.length} file(s))\n`);
|
|
128
75
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
76
|
+
const mod = createValidatorSystemModule();
|
|
77
|
+
const results = await mod.runL2ValidatorsUseCase.execute({
|
|
78
|
+
targetPaths: tsFiles,
|
|
79
|
+
unitName: '',
|
|
80
|
+
currentPhase: '',
|
|
81
|
+
});
|
|
134
82
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
83
|
+
const report = buildReport(results);
|
|
84
|
+
process.stdout.write(`${new HumanValidationResultFormatter().format(report)}\n`);
|
|
139
85
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
allErrors.push(...toHarnessErrors(errors, entry));
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Report results
|
|
146
|
-
const errorCount = allErrors.filter((e) => e.severity === "error").length;
|
|
147
|
-
|
|
148
|
-
if (allErrors.length > 0) {
|
|
149
|
-
console.log();
|
|
150
|
-
console.log(formatForHuman(allErrors, config));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (errorCount > 0) {
|
|
154
|
-
console.log(`\n${RED}${BOLD}[harness] Commit blocked.${RESET}`);
|
|
86
|
+
if (!report.overallPassed) {
|
|
87
|
+
process.stdout.write(`\n${RED}${BOLD}[phasegate] Commit blocked.${RESET}\n`);
|
|
155
88
|
process.exit(1);
|
|
156
89
|
}
|
|
157
|
-
|
|
158
|
-
|
|
90
|
+
process.stdout.write(`\n${GREEN}[phasegate]${RESET} All checks passed.\n`);
|
|
91
|
+
process.exit(0);
|
|
159
92
|
}
|
|
160
93
|
|
|
161
94
|
main().catch((err) => {
|
|
162
|
-
|
|
163
|
-
process.
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
process.stderr.write(`${RED}[phasegate] Unexpected error:${RESET} ${msg}\n`);
|
|
97
|
+
process.exit(2);
|
|
164
98
|
});
|
package/scripts/harness/main.ts
CHANGED
|
@@ -85,7 +85,7 @@ Commands:
|
|
|
85
85
|
phasegate:complete-check Complete L2-L4 check (--json)
|
|
86
86
|
phasegate:impact-analysis Impact analysis for story (<storyId>, --json)
|
|
87
87
|
|
|
88
|
-
ci:generate-template Generate CI template (--preset <id>, --type <
|
|
88
|
+
ci:generate-template Generate CI template (--preset <id>, --type <aidlc-gate|consistency-check|pre-commit>, --render, --json)
|
|
89
89
|
ci:migrate-agents-md Migrate AGENTS.md (--dry-run, --validate-only, --json)
|
|
90
90
|
ci:check-repetition Check error repetition (--code <errorCode>, --reset, --json)
|
|
91
91
|
|
|
@@ -698,8 +698,29 @@ async function main(): Promise<void> {
|
|
|
698
698
|
}
|
|
699
699
|
|
|
700
700
|
case 'phasegate:check-phase': {
|
|
701
|
+
// ISSUE-005 P2-6: --help / --json を positional として食わないようにする
|
|
702
|
+
if (hasFlag(args, '--help')) {
|
|
703
|
+
process.stdout.write([
|
|
704
|
+
'Usage: phasegate phasegate:check-phase [options]',
|
|
705
|
+
'',
|
|
706
|
+
'Check phase gate for a specific unit.',
|
|
707
|
+
'',
|
|
708
|
+
'Options:',
|
|
709
|
+
' --unit <unitId> Target unit ID (e.g., harness-api). If omitted,',
|
|
710
|
+
' the first positional argument is used.',
|
|
711
|
+
' --json Output result as JSON.',
|
|
712
|
+
' --help Show this help.',
|
|
713
|
+
'',
|
|
714
|
+
'Examples:',
|
|
715
|
+
' phasegate phasegate:check-phase --unit harness-api',
|
|
716
|
+
' phasegate phasegate:check-phase harness-api --json',
|
|
717
|
+
'',
|
|
718
|
+
].join('\n'));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
701
721
|
const mod = createHarnessApiModule();
|
|
702
|
-
const
|
|
722
|
+
const positional = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
723
|
+
const unit = parseFlag(args, '--unit') ?? positional ?? '';
|
|
703
724
|
const flags: Record<string, boolean | string> = {};
|
|
704
725
|
if (json) flags.json = true;
|
|
705
726
|
await mod.handlers.checkPhase.handle({ unit }, flags);
|
|
@@ -760,6 +781,25 @@ async function main(): Promise<void> {
|
|
|
760
781
|
|
|
761
782
|
// ── ci-governance ──
|
|
762
783
|
case 'ci:generate-template': {
|
|
784
|
+
if (hasFlag(args, '--help')) {
|
|
785
|
+
console.log(`Usage: phasegate ci:generate-template [options]
|
|
786
|
+
|
|
787
|
+
Generates a CI template configuration.
|
|
788
|
+
|
|
789
|
+
Options:
|
|
790
|
+
--preset <id> Preset name (e.g. standard, strict). Required.
|
|
791
|
+
--type <type> Template purpose (NOT CI platform name). One of:
|
|
792
|
+
aidlc-gate — AIDLC phase gate checks
|
|
793
|
+
consistency-check — Doc/code consistency checks
|
|
794
|
+
pre-commit — Pre-commit hook template
|
|
795
|
+
--render Render the template to stdout
|
|
796
|
+
--json Output in JSON format
|
|
797
|
+
|
|
798
|
+
Examples:
|
|
799
|
+
phasegate ci:generate-template --preset standard --type aidlc-gate
|
|
800
|
+
phasegate ci:generate-template --preset strict --type pre-commit --render`);
|
|
801
|
+
process.exit(0);
|
|
802
|
+
}
|
|
763
803
|
const mod = buildCiGovernance(rootDir);
|
|
764
804
|
const presetId = parseFlag(args, '--preset') ?? 'default';
|
|
765
805
|
const templateType = parseFlag(args, '--type') ?? 'aidlc-gate';
|
package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-document-age-adapter.ts
CHANGED
|
@@ -17,13 +17,19 @@ export class GitLogDocumentAgeAdapter implements DocumentAgePort {
|
|
|
17
17
|
constructor(
|
|
18
18
|
private readonly projectRoot: string,
|
|
19
19
|
private readonly nowProvider: () => Date = () => new Date(),
|
|
20
|
-
private readonly gitLogExecutor: (
|
|
20
|
+
private readonly gitLogExecutor: (
|
|
21
|
+
command: string,
|
|
22
|
+
options: { cwd: string; stdio?: readonly ['pipe', 'pipe', 'pipe'] },
|
|
23
|
+
) => Buffer = execSync,
|
|
21
24
|
) {}
|
|
22
25
|
|
|
23
26
|
async getAge(documentPath: string): Promise<DocumentAge> {
|
|
24
27
|
try {
|
|
25
28
|
const output = this.gitLogExecutor(`git log --format=%ai -1 -- "${documentPath}"`, {
|
|
26
29
|
cwd: this.projectRoot,
|
|
30
|
+
// ISSUE-005 P1-3: fresh repo では "fatal: your current branch ... does not have
|
|
31
|
+
// any commits yet" が 34回 stderr に漏れる。pipe に束ねて静音化する。
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe'] as const,
|
|
27
33
|
})
|
|
28
34
|
.toString()
|
|
29
35
|
.trim();
|
package/scripts/harness/quick-mode/infrastructure/adapters/git-diff-changed-files-adapter.ts
CHANGED
|
@@ -42,9 +42,17 @@ export class GitDiffChangedFilesAdapter {
|
|
|
42
42
|
getChangedFiles(): readonly ChangedFile[] {
|
|
43
43
|
let output: string;
|
|
44
44
|
|
|
45
|
+
// ISSUE-005 P1-3: fresh repo (HEAD 未作成) では `git diff ... HEAD` が fatal
|
|
46
|
+
// になるため、HEAD の存在を事前確認して命令を切り替える
|
|
47
|
+
const hasHead = this.hasHead();
|
|
48
|
+
const command = hasHead
|
|
49
|
+
? 'git diff --name-status --cached HEAD'
|
|
50
|
+
: 'git diff --name-status --cached';
|
|
51
|
+
|
|
45
52
|
try {
|
|
46
|
-
output = childProcess.execSync(
|
|
53
|
+
output = childProcess.execSync(command, {
|
|
47
54
|
encoding: 'utf8',
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
56
|
}) as string;
|
|
49
57
|
} catch (err) {
|
|
50
58
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -89,4 +97,15 @@ export class GitDiffChangedFilesAdapter {
|
|
|
89
97
|
|
|
90
98
|
return Object.freeze(files);
|
|
91
99
|
}
|
|
100
|
+
|
|
101
|
+
private hasHead(): boolean {
|
|
102
|
+
try {
|
|
103
|
+
childProcess.execSync('git rev-parse --verify HEAD', {
|
|
104
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
105
|
+
});
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
92
111
|
}
|
|
@@ -40,7 +40,10 @@ export function buildRegressionSuite(baseDir: string): RegressionSuiteCompositio
|
|
|
40
40
|
const suiteRegistryPort = new StaticSuiteRegistryAdapter();
|
|
41
41
|
const testRunnerPort = new VitestTestRunnerAdapter();
|
|
42
42
|
const configQueryPort = new HarnessConfigQueryAdapter();
|
|
43
|
-
|
|
43
|
+
// ISSUE-005 P2-7: リポジトリ直下ではなく reports/regression/ 配下に出力する
|
|
44
|
+
const ciGateResultWriterPort = new JsonCiGateResultWriterAdapter(
|
|
45
|
+
path.join(baseDir, 'reports', 'regression'),
|
|
46
|
+
);
|
|
44
47
|
const importAnalyzerPort = new BiomeAstImportAnalyzerAdapter();
|
|
45
48
|
const v0SpecReaderPort = new FileSystemV0SpecReaderAdapter(baseDir);
|
|
46
49
|
const migrationMappingRepositoryPort = new MarkdownMigrationMappingRepositoryAdapter(
|
|
@@ -13,4 +13,9 @@ export interface RunFullValidationInput {
|
|
|
13
13
|
readonly failOnWarning?: boolean;
|
|
14
14
|
readonly coverageReportPath?: string;
|
|
15
15
|
readonly requirementMatrixPath?: string;
|
|
16
|
+
/**
|
|
17
|
+
* ISSUE-005 P1-4: 実行レイヤー絞り込み。未指定は全レイヤー実行。
|
|
18
|
+
* 指定された場合、`includeL4` より優先される。
|
|
19
|
+
*/
|
|
20
|
+
readonly targetLayers?: readonly ('L2' | 'L3' | 'L4')[];
|
|
16
21
|
}
|
package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts
CHANGED
|
@@ -32,22 +32,50 @@ export class RunFullValidationUseCase {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async execute(input: RunFullValidationInput): Promise<AggregatedValidationReport> {
|
|
35
|
+
// ISSUE-005 P1-4: targetLayers で絞り込み。未指定時は従来の includeL4 挙動を維持。
|
|
35
36
|
const includeL4 = input.includeL4 !== false;
|
|
37
|
+
const defaultLayers: readonly ('L2' | 'L3' | 'L4')[] = includeL4
|
|
38
|
+
? ['L2', 'L3', 'L4']
|
|
39
|
+
: ['L2', 'L3'];
|
|
40
|
+
const effectiveLayers = input.targetLayers ?? defaultLayers;
|
|
41
|
+
const runL2 = effectiveLayers.includes('L2');
|
|
42
|
+
const runL3 = effectiveLayers.includes('L3');
|
|
43
|
+
const runL4 = effectiveLayers.includes('L4');
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
type Result = {
|
|
46
|
+
validatorId: string;
|
|
47
|
+
passed: boolean;
|
|
48
|
+
errors: readonly {
|
|
49
|
+
code: string;
|
|
50
|
+
severity: string;
|
|
51
|
+
message: string;
|
|
52
|
+
suggestion: string;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}[];
|
|
55
|
+
durationMs: number;
|
|
56
|
+
skipped?: boolean;
|
|
57
|
+
};
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
let l2Results: readonly Result[] = [];
|
|
60
|
+
if (runL2) {
|
|
61
|
+
l2Results = await this.l2UseCase.execute({
|
|
62
|
+
targetPaths: input.targetPaths,
|
|
63
|
+
unitName: input.unitName,
|
|
64
|
+
currentPhase: input.currentPhase,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let l3Results: readonly Result[] = [];
|
|
69
|
+
if (runL3) {
|
|
70
|
+
l3Results = await this.l3UseCase.execute({
|
|
71
|
+
targetPaths: input.targetPaths,
|
|
72
|
+
coverageReportPath: input.coverageReportPath,
|
|
73
|
+
requirementMatrixPath: input.requirementMatrixPath,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
48
76
|
|
|
49
|
-
let l4Results: readonly
|
|
50
|
-
if (
|
|
77
|
+
let l4Results: readonly Result[] = [];
|
|
78
|
+
if (runL4) {
|
|
51
79
|
l4Results = await this.l4UseCase.execute({
|
|
52
80
|
targetUnits: input.targetUnits,
|
|
53
81
|
});
|
|
@@ -100,6 +100,7 @@ export interface ValidatorSystemModule {
|
|
|
100
100
|
runQuickModeUseCase: RunQuickModeUseCase;
|
|
101
101
|
aggregateValidationResultsUseCase: AggregateValidationResultsUseCase;
|
|
102
102
|
runFullValidationUseCase: RunFullValidationUseCase;
|
|
103
|
+
driftDetectionService: DriftDetectionService;
|
|
103
104
|
handlers: {
|
|
104
105
|
runValidators: RunValidatorsHandler;
|
|
105
106
|
runQuickMode: RunQuickModeHandler;
|
|
@@ -229,6 +230,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
229
230
|
runQuickModeUseCase,
|
|
230
231
|
aggregateValidationResultsUseCase,
|
|
231
232
|
runFullValidationUseCase,
|
|
233
|
+
driftDetectionService,
|
|
232
234
|
handlers,
|
|
233
235
|
};
|
|
234
236
|
}
|
|
@@ -9,10 +9,20 @@ import { DriftReport } from '../../value-objects/drift-report.js';
|
|
|
9
9
|
|
|
10
10
|
export interface DriftDetectionDesignDocumentPort {
|
|
11
11
|
getElements(targetUnits?: readonly string[]): Promise<string[]>;
|
|
12
|
+
/**
|
|
13
|
+
* ISSUE-005 P3-9: element → unit 名のマップ。
|
|
14
|
+
* 実装されていれば DriftReport.unitName の解決に使われる (fallback: 'unknown')。
|
|
15
|
+
*/
|
|
16
|
+
getElementUnitMap?(targetUnits?: readonly string[]): Promise<Record<string, string>>;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
export interface DriftDetectionSourceCodeAnalyzerPort {
|
|
15
20
|
getElements(targetUnits?: readonly string[]): Promise<string[]>;
|
|
21
|
+
/**
|
|
22
|
+
* ISSUE-005 P3-9: element → unit 名のマップ。
|
|
23
|
+
* 実装されていれば DriftReport.unitName の解決に使われる (fallback: 'unknown')。
|
|
24
|
+
*/
|
|
25
|
+
getElementUnitMap?(targetUnits?: readonly string[]): Promise<Record<string, string>>;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
export interface DriftDetectionServiceDeps {
|
|
@@ -33,6 +43,23 @@ export class DriftDetectionService {
|
|
|
33
43
|
const designElements = await this.designDocumentPort.getElements(targetUnits);
|
|
34
44
|
const codeElements = await this.sourceCodeAnalyzerPort.getElements(targetUnits);
|
|
35
45
|
|
|
46
|
+
// ISSUE-005 P3-9: element → unit のマップを取得し、DriftReport.unitName の解決に使う
|
|
47
|
+
const designUnitMap = this.designDocumentPort.getElementUnitMap
|
|
48
|
+
? await this.designDocumentPort.getElementUnitMap(targetUnits)
|
|
49
|
+
: {};
|
|
50
|
+
const codeUnitMap = this.sourceCodeAnalyzerPort.getElementUnitMap
|
|
51
|
+
? await this.sourceCodeAnalyzerPort.getElementUnitMap(targetUnits)
|
|
52
|
+
: {};
|
|
53
|
+
|
|
54
|
+
const resolveUnit = (element: string): string => {
|
|
55
|
+
return (
|
|
56
|
+
designUnitMap[element] ??
|
|
57
|
+
codeUnitMap[element] ??
|
|
58
|
+
targetUnits?.[0] ??
|
|
59
|
+
'unknown'
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
36
63
|
const designSet = new Set(designElements);
|
|
37
64
|
const codeSet = new Set(codeElements);
|
|
38
65
|
|
|
@@ -44,7 +71,7 @@ export class DriftDetectionService {
|
|
|
44
71
|
reports.push(
|
|
45
72
|
DriftReport.create({
|
|
46
73
|
direction: 'design→code',
|
|
47
|
-
unitName: (
|
|
74
|
+
unitName: resolveUnit(element),
|
|
48
75
|
element,
|
|
49
76
|
description: `設計に存在するがコードに存在しない: ${element}`,
|
|
50
77
|
recommendation: `${element} をコードに実装してください`,
|
|
@@ -59,7 +86,7 @@ export class DriftDetectionService {
|
|
|
59
86
|
reports.push(
|
|
60
87
|
DriftReport.create({
|
|
61
88
|
direction: 'code→design',
|
|
62
|
-
unitName: (
|
|
89
|
+
unitName: resolveUnit(element),
|
|
63
90
|
element,
|
|
64
91
|
description: `コードに存在するが設計に存在しない: ${element}`,
|
|
65
92
|
recommendation: `${element} を設計文書に追記するか、コードから削除してください`,
|
|
@@ -44,6 +44,23 @@ export class BiomeAstSourceCodeAnalyzerAdapter implements SourceCodeAnalyzerPort
|
|
|
44
44
|
const results = await this.analyzeExports(targetUnits);
|
|
45
45
|
return results.flatMap((result) => result.exports.map((entry) => entry.name));
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ISSUE-005 P3-9: element 名から unit 名を引けるマップを返す。
|
|
50
|
+
* 同名 export が複数 unit に存在する場合は最初に見つかった unit を採用する。
|
|
51
|
+
*/
|
|
52
|
+
async getElementUnitMap(targetUnits?: readonly string[]): Promise<Record<string, string>> {
|
|
53
|
+
const results = await this.analyzeExports(targetUnits);
|
|
54
|
+
const map: Record<string, string> = {};
|
|
55
|
+
for (const result of results) {
|
|
56
|
+
for (const entry of result.exports) {
|
|
57
|
+
if (!(entry.name in map)) {
|
|
58
|
+
map[entry.name] = result.unitName;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
type ExportType = SourceAnalysisResult['exports'][number]['type'];
|
package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts
CHANGED
|
@@ -8,9 +8,55 @@ import type { DesignDocumentPort, StructuredDesignDoc } from '../../domain/ports
|
|
|
8
8
|
import { readFile, readdir } from 'node:fs/promises';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
|
|
11
|
-
const SECTION_PATTERN = /^#{2,3}\s+(.+)/gm;
|
|
12
11
|
const ADR_PATTERN = /ADR-\d{3}/g;
|
|
13
12
|
|
|
13
|
+
// ISSUE-005 P3-8: メタ見出し / 議論用セクションを drift 対象から除外するマーカー。
|
|
14
|
+
// 見出し行の直後 (同一行末 or 次の非空行) に置かれたコメントを拾う。
|
|
15
|
+
const SKIP_MARKER = /<!--\s*@drift-check\s*:\s*skip\s*-->/i;
|
|
16
|
+
|
|
17
|
+
// 見出し文字列のうち、デフォルトで drift 対象から外す既知のメタパターン。
|
|
18
|
+
// Unit 設計ドキュメントで頻出する「議論用」「自己評価」系セクション。
|
|
19
|
+
const DEFAULT_META_HEADING_PATTERNS = [
|
|
20
|
+
/engineering[- ]perspective/i,
|
|
21
|
+
/自己評価/,
|
|
22
|
+
/レビュー(観点|コメント)/,
|
|
23
|
+
/議論/,
|
|
24
|
+
/TODO|未決事項|Open\s+Questions?/i,
|
|
25
|
+
/変更履歴|Change\s*Log/i,
|
|
26
|
+
/参考文献|References?/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function isMetaHeading(name: string): boolean {
|
|
30
|
+
return DEFAULT_META_HEADING_PATTERNS.some((p) => p.test(name));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractConceptNames(markdown: string): string[] {
|
|
34
|
+
const lines = markdown.split(/\r?\n/);
|
|
35
|
+
const headings: string[] = [];
|
|
36
|
+
const headingRegex = /^(#{2,3})\s+(.+?)\s*$/;
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const m = headingRegex.exec(lines[i]);
|
|
39
|
+
if (!m) continue;
|
|
40
|
+
const name = m[2].trim();
|
|
41
|
+
// 見出し行自体にスキップマーカーが付いている
|
|
42
|
+
if (SKIP_MARKER.test(lines[i])) continue;
|
|
43
|
+
// 次の非空行にスキップマーカーが付いている
|
|
44
|
+
// ただし、次行が新しい見出しの場合はそれ自身の注釈なので無視する
|
|
45
|
+
let j = i + 1;
|
|
46
|
+
while (j < lines.length && lines[j].trim() === '') j++;
|
|
47
|
+
if (
|
|
48
|
+
j < lines.length &&
|
|
49
|
+
!headingRegex.test(lines[j]) &&
|
|
50
|
+
SKIP_MARKER.test(lines[j])
|
|
51
|
+
) continue;
|
|
52
|
+
// 既知のメタ見出しは暗黙的にスキップ
|
|
53
|
+
if (isMetaHeading(name)) continue;
|
|
54
|
+
// 名前から末尾のスキップマーカーを落とす (念のため)
|
|
55
|
+
headings.push(name.replace(SKIP_MARKER, '').trim());
|
|
56
|
+
}
|
|
57
|
+
return headings;
|
|
58
|
+
}
|
|
59
|
+
|
|
14
60
|
export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
|
|
15
61
|
private readonly docsRoot: string;
|
|
16
62
|
private readonly cache = new Map<string, StructuredDesignDoc>();
|
|
@@ -38,10 +84,7 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
|
|
|
38
84
|
const doc: StructuredDesignDoc = {
|
|
39
85
|
unitName,
|
|
40
86
|
docPath,
|
|
41
|
-
concepts:
|
|
42
|
-
name: match[1].trim(),
|
|
43
|
-
type: 'class',
|
|
44
|
-
})),
|
|
87
|
+
concepts: extractConceptNames(markdown).map((name) => ({ name, type: 'class' })),
|
|
45
88
|
layerDependencies: [],
|
|
46
89
|
adrRefs: Array.from(new Set(markdown.match(ADR_PATTERN) ?? [])),
|
|
47
90
|
};
|
|
@@ -64,6 +107,23 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
|
|
|
64
107
|
return docs.flatMap((doc) => doc.concepts.map((concept) => concept.name));
|
|
65
108
|
}
|
|
66
109
|
|
|
110
|
+
/**
|
|
111
|
+
* ISSUE-005 P3-9: element 名から unit 名を引けるマップを返す。
|
|
112
|
+
* 同名 element が複数 unit に存在する場合は最初に見つかった unit を採用する。
|
|
113
|
+
*/
|
|
114
|
+
async getElementUnitMap(targetUnits?: readonly string[]): Promise<Record<string, string>> {
|
|
115
|
+
const docs = await this.loadDesignDocuments(targetUnits);
|
|
116
|
+
const map: Record<string, string> = {};
|
|
117
|
+
for (const doc of docs) {
|
|
118
|
+
for (const concept of doc.concepts) {
|
|
119
|
+
if (!(concept.name in map)) {
|
|
120
|
+
map[concept.name] = doc.unitName;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return map;
|
|
125
|
+
}
|
|
126
|
+
|
|
67
127
|
private async listUnitNames(): Promise<string[]> {
|
|
68
128
|
try {
|
|
69
129
|
const entries = await readdir(this.docsRoot, { withFileTypes: true });
|
|
@@ -110,12 +110,21 @@ export class RunValidatorsHandler {
|
|
|
110
110
|
return { output, exitCode: l1Report.overallPassed ? 0 : 1 };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// ISSUE-005 P1-4: args.layer を targetLayers にマップ
|
|
114
|
+
let targetLayers: readonly ('L2' | 'L3' | 'L4')[] | undefined;
|
|
115
|
+
if (args.layer === 'L2') targetLayers = ['L2'];
|
|
116
|
+
else if (args.layer === 'L3') targetLayers = ['L3'];
|
|
117
|
+
else if (args.layer === 'L4') targetLayers = ['L4'];
|
|
118
|
+
else if (args.layer === 'all') targetLayers = ['L2', 'L3', 'L4'];
|
|
119
|
+
// undefined → フィルタなし(従来挙動)
|
|
120
|
+
|
|
113
121
|
const report = await this.useCase.execute({
|
|
114
122
|
targetPaths: args.targetPaths ?? [],
|
|
115
123
|
unitName: args.unit ?? '',
|
|
116
124
|
currentPhase: args.phase ?? '',
|
|
117
125
|
includeL4: !args.noL4,
|
|
118
126
|
failOnWarning: args.failOnWarning,
|
|
127
|
+
targetLayers,
|
|
119
128
|
});
|
|
120
129
|
|
|
121
130
|
const format = args.format ?? 'human';
|