phasegate 0.62.0 → 0.63.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/package.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +15 -0
- package/scripts/harness/main.ts +35 -0
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +20 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +83 -0
- package/scripts/harness/quick-mode/composition-root.ts +12 -0
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +38 -32
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +36 -4
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +6 -0
- package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +42 -0
- package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +50 -0
package/package.json
CHANGED
package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json
CHANGED
|
@@ -153,6 +153,21 @@
|
|
|
153
153
|
"type": "string"
|
|
154
154
|
},
|
|
155
155
|
"uniqueItems": true
|
|
156
|
+
},
|
|
157
|
+
"fullModeRequiredWhen": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"properties": {
|
|
161
|
+
"mixedCategories": {
|
|
162
|
+
"type": "boolean"
|
|
163
|
+
},
|
|
164
|
+
"newDomainFile": {
|
|
165
|
+
"type": "boolean"
|
|
166
|
+
},
|
|
167
|
+
"apiContractChange": {
|
|
168
|
+
"type": "boolean"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
156
171
|
}
|
|
157
172
|
}
|
|
158
173
|
},
|
package/scripts/harness/main.ts
CHANGED
|
@@ -716,6 +716,41 @@ async function main(): Promise<void> {
|
|
|
716
716
|
break;
|
|
717
717
|
}
|
|
718
718
|
|
|
719
|
+
// ── quick-mode / check-change-category (H10-05) ──
|
|
720
|
+
case 'check-change-category': {
|
|
721
|
+
if (hasFlag(args, '--help')) {
|
|
722
|
+
process.stdout.write([
|
|
723
|
+
'Usage: phasegate check-change-category --paths <csv> [options]',
|
|
724
|
+
'',
|
|
725
|
+
'Classify changed file paths into quick-mode categories and report',
|
|
726
|
+
'whether Full Mode is required.',
|
|
727
|
+
'',
|
|
728
|
+
'Options:',
|
|
729
|
+
' --paths <csv> Comma-separated file paths to classify.',
|
|
730
|
+
' --format <human|json> Output format. Default: human.',
|
|
731
|
+
' --fail-on-full-required Exit with code 1 when Full Mode is required.',
|
|
732
|
+
' --help Show this help.',
|
|
733
|
+
'',
|
|
734
|
+
'Examples:',
|
|
735
|
+
' phasegate check-change-category --paths src/foo.ts,src/bar.ts',
|
|
736
|
+
' phasegate check-change-category --paths src/foo.ts --format json',
|
|
737
|
+
'',
|
|
738
|
+
].join('\n'));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const mod = createQuickModeCompositionRoot();
|
|
742
|
+
const paths = parseFlag(args, '--paths');
|
|
743
|
+
const format = parseFlag(args, '--format') as 'human' | 'json' | undefined;
|
|
744
|
+
const failOnFullRequired = hasFlag(args, '--fail-on-full-required');
|
|
745
|
+
const result = await mod.checkChangeCategoryHandler.handle({
|
|
746
|
+
paths,
|
|
747
|
+
format,
|
|
748
|
+
failOnFullRequired,
|
|
749
|
+
});
|
|
750
|
+
process.exit(result.exitCode);
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
|
|
719
754
|
// ── harness-api ──
|
|
720
755
|
case 'phasegate:check-ready': {
|
|
721
756
|
const mod = createHarnessApiModule();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer application
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @story H10-05
|
|
5
|
+
*
|
|
6
|
+
* ChangeCategoryClassification の公開 DTO
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ChangeCategoryPerFile {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly category: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ChangeCategoryClassificationContract {
|
|
15
|
+
readonly dominantCategory: string | null;
|
|
16
|
+
readonly perFile: readonly ChangeCategoryPerFile[];
|
|
17
|
+
readonly fullModeRequired: boolean;
|
|
18
|
+
readonly rejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
19
|
+
readonly rejectionReason?: string;
|
|
20
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer application
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @story H10-05
|
|
5
|
+
*
|
|
6
|
+
* paths から変更カテゴリを分類し fullModeRequired 判定と理由を返す UseCase
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ChangedFile } from '../../domain/value-objects/changed-file.js';
|
|
10
|
+
import { QuickModeJudgmentEngine } from '../../domain/services/quick-mode-judgment-engine.js';
|
|
11
|
+
import type { QuickModeConfigPort } from '../ports/quick-mode-config-port.js';
|
|
12
|
+
import type { ChangeCategoryClassificationContract, ChangeCategoryPerFile } from '../dto/change-category-classification-contract.js';
|
|
13
|
+
|
|
14
|
+
export interface ClassifyChangeCategoryUseCaseInput {
|
|
15
|
+
readonly paths: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ClassifyChangeCategoryUseCaseDeps {
|
|
19
|
+
quickModeConfigPort: QuickModeConfigPort;
|
|
20
|
+
judgmentEngine?: QuickModeJudgmentEngine;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ClassifyChangeCategoryUseCase {
|
|
24
|
+
private readonly quickModeConfigPort: QuickModeConfigPort;
|
|
25
|
+
private readonly judgmentEngine: QuickModeJudgmentEngine;
|
|
26
|
+
|
|
27
|
+
constructor(deps: ClassifyChangeCategoryUseCaseDeps) {
|
|
28
|
+
this.quickModeConfigPort = deps.quickModeConfigPort;
|
|
29
|
+
this.judgmentEngine = deps.judgmentEngine ?? new QuickModeJudgmentEngine();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async execute(
|
|
33
|
+
input: ClassifyChangeCategoryUseCaseInput
|
|
34
|
+
): Promise<Readonly<ChangeCategoryClassificationContract>> {
|
|
35
|
+
const config = await this.quickModeConfigPort.getConfig();
|
|
36
|
+
|
|
37
|
+
if (input.paths.length === 0) {
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
dominantCategory: null,
|
|
40
|
+
perFile: [],
|
|
41
|
+
fullModeRequired: false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const changedFiles = input.paths.map((p) =>
|
|
46
|
+
ChangedFile.create({ filePath: p, changeKind: 'MODIFY' })
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const classification = this.judgmentEngine.classify(changedFiles, config);
|
|
50
|
+
const eligibility = this.judgmentEngine.judge(changedFiles, config);
|
|
51
|
+
|
|
52
|
+
const perFile: ChangeCategoryPerFile[] = [];
|
|
53
|
+
for (const path of input.paths) {
|
|
54
|
+
const match = changedFiles.find((f) => f.filePath === path);
|
|
55
|
+
if (!match) continue;
|
|
56
|
+
let categoryForFile: string | null = null;
|
|
57
|
+
classification.categorizedFiles.forEach((files, categoryKey) => {
|
|
58
|
+
if (files.some((f) => f.filePath === path)) {
|
|
59
|
+
categoryForFile = categoryKey;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
perFile.push({ path, category: categoryForFile ?? 'unknown' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const fullModeRequired = !eligibility.isEligible();
|
|
66
|
+
|
|
67
|
+
const contract: ChangeCategoryClassificationContract = fullModeRequired
|
|
68
|
+
? {
|
|
69
|
+
dominantCategory: classification.dominantCategory?.toString() ?? null,
|
|
70
|
+
perFile,
|
|
71
|
+
fullModeRequired: true,
|
|
72
|
+
rejectionRule: eligibility.rejectionRule,
|
|
73
|
+
rejectionReason: eligibility.reason,
|
|
74
|
+
}
|
|
75
|
+
: {
|
|
76
|
+
dominantCategory: classification.dominantCategory?.toString() ?? null,
|
|
77
|
+
perFile,
|
|
78
|
+
fullModeRequired: false,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return Object.freeze(contract);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -14,13 +14,17 @@ import { QuickModeDecisionContractMapper } from './application/mappers/quick-mod
|
|
|
14
14
|
import { JudgeQuickModeEligibilityUseCase } from './application/usecases/judge-quick-mode-eligibility-usecase.js';
|
|
15
15
|
import { BuildRelaxationProfileUseCase } from './application/usecases/build-relaxation-profile-usecase.js';
|
|
16
16
|
import { ExecuteQuickCiCheckUseCase } from './application/usecases/execute-quick-ci-check-usecase.js';
|
|
17
|
+
import { ClassifyChangeCategoryUseCase } from './application/usecases/classify-change-category-usecase.js';
|
|
17
18
|
import { CiCheckQuickModeHandler } from './presentation/handlers/ci-check-quick-mode-handler.js';
|
|
19
|
+
import { CheckChangeCategoryHandler } from './presentation/handlers/check-change-category-handler.js';
|
|
18
20
|
|
|
19
21
|
export interface QuickModeCompositionRoot {
|
|
20
22
|
handler: CiCheckQuickModeHandler;
|
|
23
|
+
checkChangeCategoryHandler: CheckChangeCategoryHandler;
|
|
21
24
|
executeUseCase: ExecuteQuickCiCheckUseCase;
|
|
22
25
|
judgeUseCase: JudgeQuickModeEligibilityUseCase;
|
|
23
26
|
buildUseCase: BuildRelaxationProfileUseCase;
|
|
27
|
+
classifyUseCase: ClassifyChangeCategoryUseCase;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
|
|
@@ -62,13 +66,21 @@ export function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
|
|
|
62
66
|
buildUseCase,
|
|
63
67
|
});
|
|
64
68
|
|
|
69
|
+
const classifyUseCase = new ClassifyChangeCategoryUseCase({
|
|
70
|
+
quickModeConfigPort,
|
|
71
|
+
judgmentEngine,
|
|
72
|
+
});
|
|
73
|
+
|
|
65
74
|
// Presentation
|
|
66
75
|
const handler = new CiCheckQuickModeHandler({ useCase: executeUseCase });
|
|
76
|
+
const checkChangeCategoryHandler = new CheckChangeCategoryHandler({ useCase: classifyUseCase });
|
|
67
77
|
|
|
68
78
|
return {
|
|
69
79
|
handler,
|
|
80
|
+
checkChangeCategoryHandler,
|
|
70
81
|
executeUseCase,
|
|
71
82
|
judgeUseCase,
|
|
72
83
|
buildUseCase,
|
|
84
|
+
classifyUseCase,
|
|
73
85
|
};
|
|
74
86
|
}
|
|
@@ -100,47 +100,53 @@ export class QuickModeJudgmentEngine {
|
|
|
100
100
|
const classification = this.classify(changedFiles, config);
|
|
101
101
|
|
|
102
102
|
// 1. MIXED_CHANGES評価: allowedCategories 外のカテゴリが含まれる場合
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
if (config.isFullModeRequiredFor('mixedCategories')) {
|
|
104
|
+
const notAllowedFiles: ChangedFile[] = [];
|
|
105
|
+
classification.categorizedFiles.forEach((files, categoryKey) => {
|
|
106
|
+
if (!config.isAllowed(categoryKey)) {
|
|
107
|
+
notAllowedFiles.push(...files);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (notAllowedFiles.length > 0) {
|
|
112
|
+
return QuickModeEligibility.rejected(
|
|
113
|
+
'MIXED_CHANGES',
|
|
114
|
+
notAllowedFiles,
|
|
115
|
+
`allowedCategories外のファイルが含まれています: ${notAllowedFiles.map((f) => f.filePath).join(', ')}`
|
|
116
|
+
);
|
|
107
117
|
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
if (notAllowedFiles.length > 0) {
|
|
111
|
-
return QuickModeEligibility.rejected(
|
|
112
|
-
'MIXED_CHANGES',
|
|
113
|
-
notAllowedFiles,
|
|
114
|
-
`allowedCategories外のファイルが含まれています: ${notAllowedFiles.map((f) => f.filePath).join(', ')}`
|
|
115
|
-
);
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
// 2. NEW_DOMAIN評価: domain/ 配下かつ changeKind=CREATE
|
|
119
|
-
|
|
120
|
-
(
|
|
121
|
-
(f
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (newDomainFiles.length > 0) {
|
|
126
|
-
return QuickModeEligibility.rejected(
|
|
127
|
-
'NEW_DOMAIN',
|
|
128
|
-
newDomainFiles,
|
|
129
|
-
`domain/配下に新規ファイルが追加されています: ${newDomainFiles.map((f) => f.filePath).join(', ')}`
|
|
121
|
+
if (config.isFullModeRequiredFor('newDomainFile')) {
|
|
122
|
+
const newDomainFiles = changedFiles.filter(
|
|
123
|
+
(f) =>
|
|
124
|
+
(f.filePath.includes('/domain/') || f.filePath.startsWith('domain/')) &&
|
|
125
|
+
f.changeKind === 'CREATE'
|
|
130
126
|
);
|
|
127
|
+
|
|
128
|
+
if (newDomainFiles.length > 0) {
|
|
129
|
+
return QuickModeEligibility.rejected(
|
|
130
|
+
'NEW_DOMAIN',
|
|
131
|
+
newDomainFiles,
|
|
132
|
+
`domain/配下に新規ファイルが追加されています: ${newDomainFiles.map((f) => f.filePath).join(', ')}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
// 3. API_CONTRACT評価: *port.ts / *adapter.ts の変更
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (apiContractFiles.length > 0) {
|
|
139
|
-
return QuickModeEligibility.rejected(
|
|
140
|
-
'API_CONTRACT',
|
|
141
|
-
apiContractFiles,
|
|
142
|
-
`Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles.map((f) => f.filePath).join(', ')}`
|
|
138
|
+
if (config.isFullModeRequiredFor('apiContractChange')) {
|
|
139
|
+
const apiContractFiles = changedFiles.filter(
|
|
140
|
+
(f) => f.filePath.endsWith('port.ts') || f.filePath.endsWith('adapter.ts')
|
|
143
141
|
);
|
|
142
|
+
|
|
143
|
+
if (apiContractFiles.length > 0) {
|
|
144
|
+
return QuickModeEligibility.rejected(
|
|
145
|
+
'API_CONTRACT',
|
|
146
|
+
apiContractFiles,
|
|
147
|
+
`Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles.map((f) => f.filePath).join(', ')}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
return QuickModeEligibility.eligible('すべてのファイルが許可カテゴリ内です');
|
|
@@ -12,20 +12,37 @@ export class QuickModeConfigError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export type FullModeRequiredRuleId = 'mixedCategories' | 'newDomainFile' | 'apiContractChange';
|
|
16
|
+
|
|
17
|
+
export interface FullModeRequiredRules {
|
|
18
|
+
readonly mixedCategories: boolean;
|
|
19
|
+
readonly newDomainFile: boolean;
|
|
20
|
+
readonly apiContractChange: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_FULL_MODE_REQUIRED_WHEN: FullModeRequiredRules = Object.freeze({
|
|
24
|
+
mixedCategories: true,
|
|
25
|
+
newDomainFile: true,
|
|
26
|
+
apiContractChange: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
15
29
|
|
|
16
30
|
export class QuickModeConfig {
|
|
17
31
|
readonly allowedCategories: readonly string[];
|
|
18
32
|
readonly maintainedLayers: readonly string[];
|
|
19
33
|
readonly relaxedGates: readonly string[];
|
|
34
|
+
readonly fullModeRequiredWhen: FullModeRequiredRules;
|
|
20
35
|
|
|
21
36
|
private constructor(
|
|
22
37
|
allowedCategories: readonly string[],
|
|
23
38
|
maintainedLayers: readonly string[],
|
|
24
|
-
relaxedGates: readonly string[]
|
|
39
|
+
relaxedGates: readonly string[],
|
|
40
|
+
fullModeRequiredWhen: FullModeRequiredRules
|
|
25
41
|
) {
|
|
26
42
|
this.allowedCategories = allowedCategories;
|
|
27
43
|
this.maintainedLayers = maintainedLayers;
|
|
28
44
|
this.relaxedGates = relaxedGates;
|
|
45
|
+
this.fullModeRequiredWhen = fullModeRequiredWhen;
|
|
29
46
|
Object.freeze(this);
|
|
30
47
|
}
|
|
31
48
|
|
|
@@ -33,17 +50,25 @@ export class QuickModeConfig {
|
|
|
33
50
|
allowedCategories: string[];
|
|
34
51
|
maintainedLayers: string[];
|
|
35
52
|
relaxedGates: string[];
|
|
53
|
+
fullModeRequiredWhen?: Partial<FullModeRequiredRules>;
|
|
36
54
|
}): QuickModeConfig {
|
|
37
|
-
const { allowedCategories, maintainedLayers, relaxedGates } = raw;
|
|
55
|
+
const { allowedCategories, maintainedLayers, relaxedGates, fullModeRequiredWhen } = raw;
|
|
38
56
|
|
|
39
57
|
if (allowedCategories.length === 0) {
|
|
40
58
|
throw new QuickModeConfigError('allowedCategories must not be empty');
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
const mergedRules: FullModeRequiredRules = Object.freeze({
|
|
62
|
+
mixedCategories: fullModeRequiredWhen?.mixedCategories ?? DEFAULT_FULL_MODE_REQUIRED_WHEN.mixedCategories,
|
|
63
|
+
newDomainFile: fullModeRequiredWhen?.newDomainFile ?? DEFAULT_FULL_MODE_REQUIRED_WHEN.newDomainFile,
|
|
64
|
+
apiContractChange: fullModeRequiredWhen?.apiContractChange ?? DEFAULT_FULL_MODE_REQUIRED_WHEN.apiContractChange,
|
|
65
|
+
});
|
|
66
|
+
|
|
43
67
|
return new QuickModeConfig(
|
|
44
68
|
Object.freeze([...allowedCategories]),
|
|
45
69
|
Object.freeze([...maintainedLayers]),
|
|
46
|
-
Object.freeze([...relaxedGates])
|
|
70
|
+
Object.freeze([...relaxedGates]),
|
|
71
|
+
mergedRules
|
|
47
72
|
);
|
|
48
73
|
}
|
|
49
74
|
|
|
@@ -59,11 +84,18 @@ export class QuickModeConfig {
|
|
|
59
84
|
return this.relaxedGates.includes(validatorId);
|
|
60
85
|
}
|
|
61
86
|
|
|
87
|
+
isFullModeRequiredFor(rule: FullModeRequiredRuleId): boolean {
|
|
88
|
+
return this.fullModeRequiredWhen[rule];
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
equals(other: QuickModeConfig): boolean {
|
|
63
92
|
return (
|
|
64
93
|
JSON.stringify(this.allowedCategories) === JSON.stringify(other.allowedCategories) &&
|
|
65
94
|
JSON.stringify(this.maintainedLayers) === JSON.stringify(other.maintainedLayers) &&
|
|
66
|
-
JSON.stringify(this.relaxedGates) === JSON.stringify(other.relaxedGates)
|
|
95
|
+
JSON.stringify(this.relaxedGates) === JSON.stringify(other.relaxedGates) &&
|
|
96
|
+
this.fullModeRequiredWhen.mixedCategories === other.fullModeRequiredWhen.mixedCategories &&
|
|
97
|
+
this.fullModeRequiredWhen.newDomainFile === other.fullModeRequiredWhen.newDomainFile &&
|
|
98
|
+
this.fullModeRequiredWhen.apiContractChange === other.fullModeRequiredWhen.apiContractChange
|
|
67
99
|
);
|
|
68
100
|
}
|
|
69
101
|
}
|
|
@@ -60,6 +60,11 @@ export class HarnessConfigQuickModeConfigAdapter {
|
|
|
60
60
|
allowedCategories?: string[];
|
|
61
61
|
maintainedLayers?: string[];
|
|
62
62
|
relaxedGates?: string[];
|
|
63
|
+
fullModeRequiredWhen?: {
|
|
64
|
+
mixedCategories?: boolean;
|
|
65
|
+
newDomainFile?: boolean;
|
|
66
|
+
apiContractChange?: boolean;
|
|
67
|
+
};
|
|
63
68
|
} | undefined;
|
|
64
69
|
|
|
65
70
|
if (!quickMode) {
|
|
@@ -70,6 +75,7 @@ export class HarnessConfigQuickModeConfigAdapter {
|
|
|
70
75
|
allowedCategories: quickMode.allowedCategories ?? DEFAULT_QUICK_MODE_CONFIG.allowedCategories,
|
|
71
76
|
maintainedLayers: quickMode.maintainedLayers ?? DEFAULT_QUICK_MODE_CONFIG.maintainedLayers,
|
|
72
77
|
relaxedGates: quickMode.relaxedGates ?? DEFAULT_QUICK_MODE_CONFIG.relaxedGates,
|
|
78
|
+
fullModeRequiredWhen: quickMode.fullModeRequiredWhen,
|
|
73
79
|
});
|
|
74
80
|
}
|
|
75
81
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer presentation
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @story H10-05
|
|
5
|
+
*
|
|
6
|
+
* ClassifyChangeCategoryUseCase の出力を human / json フォーマットで整形する
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ChangeCategoryClassificationContract } from '../../application/dto/change-category-classification-contract.js';
|
|
10
|
+
|
|
11
|
+
export type ChangeCategoryOutputFormat = 'human' | 'json';
|
|
12
|
+
|
|
13
|
+
export class ChangeCategoryFormatter {
|
|
14
|
+
format(
|
|
15
|
+
contract: ChangeCategoryClassificationContract,
|
|
16
|
+
format: ChangeCategoryOutputFormat = 'human'
|
|
17
|
+
): string {
|
|
18
|
+
if (format === 'json') {
|
|
19
|
+
return JSON.stringify(contract, null, 2) + '\n';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lines: string[] = [];
|
|
23
|
+
lines.push(`dominantCategory: ${contract.dominantCategory ?? '(none)'}`);
|
|
24
|
+
lines.push(`fullModeRequired: ${contract.fullModeRequired}`);
|
|
25
|
+
|
|
26
|
+
if (contract.fullModeRequired) {
|
|
27
|
+
lines.push(`rejectionRule: ${contract.rejectionRule ?? ''}`);
|
|
28
|
+
if (contract.rejectionReason) {
|
|
29
|
+
lines.push(`rejectionReason: ${contract.rejectionReason}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (contract.perFile.length > 0) {
|
|
34
|
+
lines.push('perFile:');
|
|
35
|
+
for (const entry of contract.perFile) {
|
|
36
|
+
lines.push(` ${entry.path} -> ${entry.category}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return lines.join('\n') + '\n';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer presentation
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @story H10-05
|
|
5
|
+
*
|
|
6
|
+
* phasegate check-change-category CLI のハンドラ
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ClassifyChangeCategoryUseCase } from '../../application/usecases/classify-change-category-usecase.js';
|
|
10
|
+
import { ChangeCategoryFormatter, type ChangeCategoryOutputFormat } from '../formatters/change-category-formatter.js';
|
|
11
|
+
|
|
12
|
+
export interface CheckChangeCategoryHandlerDeps {
|
|
13
|
+
useCase: Pick<ClassifyChangeCategoryUseCase, 'execute'>;
|
|
14
|
+
writer?: (s: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CheckChangeCategoryHandlerOptions {
|
|
18
|
+
paths?: string;
|
|
19
|
+
format?: ChangeCategoryOutputFormat;
|
|
20
|
+
failOnFullRequired?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CheckChangeCategoryHandlerResult {
|
|
24
|
+
exitCode: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class CheckChangeCategoryHandler {
|
|
28
|
+
private readonly useCase: Pick<ClassifyChangeCategoryUseCase, 'execute'>;
|
|
29
|
+
private readonly writer: (s: string) => void;
|
|
30
|
+
private readonly formatter = new ChangeCategoryFormatter();
|
|
31
|
+
|
|
32
|
+
constructor(deps: CheckChangeCategoryHandlerDeps) {
|
|
33
|
+
this.useCase = deps.useCase;
|
|
34
|
+
this.writer = deps.writer ?? ((s: string) => process.stdout.write(s));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async handle(options: CheckChangeCategoryHandlerOptions): Promise<CheckChangeCategoryHandlerResult> {
|
|
38
|
+
const { paths: pathsRaw, format = 'human', failOnFullRequired = false } = options;
|
|
39
|
+
|
|
40
|
+
const paths = pathsRaw
|
|
41
|
+
? pathsRaw.split(',').map((p) => p.trim()).filter((p) => p.length > 0)
|
|
42
|
+
: [];
|
|
43
|
+
|
|
44
|
+
const contract = await this.useCase.execute({ paths });
|
|
45
|
+
this.writer(this.formatter.format(contract, format));
|
|
46
|
+
|
|
47
|
+
const exitCode = failOnFullRequired && contract.fullModeRequired ? 1 : 0;
|
|
48
|
+
return { exitCode };
|
|
49
|
+
}
|
|
50
|
+
}
|