phasegate 0.64.0 → 0.66.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/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +72 -17
- package/scripts/harness/agent-integration/domain/ports/baseline-grandfather-query-port.ts +12 -0
- package/scripts/harness/agent-integration/domain/ports/config-query-port.ts +6 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +90 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +20 -1
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +6 -0
- package/scripts/harness/ci-governance/application/dto/create-baseline-input.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/create-baseline-output.ts +15 -0
- package/scripts/harness/ci-governance/application/usecases/create-baseline-usecase.ts +95 -0
- package/scripts/harness/ci-governance/composition-root.ts +21 -0
- package/scripts/harness/ci-governance/domain/ports/baseline-repository-port.ts +11 -0
- package/scripts/harness/ci-governance/domain/ports/file-hasher-port.ts +6 -0
- package/scripts/harness/ci-governance/domain/ports/file-scanner-port.ts +11 -0
- package/scripts/harness/ci-governance/domain/value-objects/baseline-entry.ts +34 -0
- package/scripts/harness/ci-governance/domain/value-objects/baseline-snapshot.ts +55 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/baseline-json-repository-adapter.ts +91 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-sha1-hasher-adapter.ts +19 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/glob-file-scanner-adapter.ts +79 -0
- package/scripts/harness/ci-governance/presentation/handlers/create-baseline-handler.ts +57 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +13 -0
- package/scripts/harness/main.ts +21 -0
package/package.json
CHANGED
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -14,6 +14,10 @@ import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
|
|
|
14
14
|
import type { PhaseGateQueryPort } from '../../domain/ports/phase-gate-query-port.js';
|
|
15
15
|
import type { StoryReflectionQueryPort } from '../../domain/ports/story-reflection-query-port.js';
|
|
16
16
|
import type { FullModeRequirementQueryPort } from '../../domain/ports/full-mode-requirement-query-port.js';
|
|
17
|
+
import type {
|
|
18
|
+
BaselineGrandfatherCheckResult,
|
|
19
|
+
BaselineGrandfatherQueryPort,
|
|
20
|
+
} from '../../domain/ports/baseline-grandfather-query-port.js';
|
|
17
21
|
import { WriteTargetScope } from '../../domain/value-objects/write-target-scope.js';
|
|
18
22
|
import type { HandlePreToolUseInput, HandlePreToolUseOutput } from '../dto/handle-pre-tool-use-dto.js';
|
|
19
23
|
|
|
@@ -22,6 +26,8 @@ export interface HandlePreToolUseUseCasePorts {
|
|
|
22
26
|
phaseGateQueryPort: PhaseGateQueryPort;
|
|
23
27
|
storyReflectionQueryPort?: StoryReflectionQueryPort;
|
|
24
28
|
fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
|
|
29
|
+
baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
|
|
30
|
+
grandfatherLogger?: (reason: string, targetFilePaths: readonly string[]) => void;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export class HandlePreToolUseInputValidationError extends Error {
|
|
@@ -41,11 +47,23 @@ export class HandlePreToolUseUseCase {
|
|
|
41
47
|
private readonly configQueryPort: ConfigQueryPort;
|
|
42
48
|
private readonly storyReflectionQueryPort?: StoryReflectionQueryPort;
|
|
43
49
|
private readonly fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
|
|
50
|
+
private readonly baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
|
|
51
|
+
private readonly grandfatherLogger: (
|
|
52
|
+
reason: string,
|
|
53
|
+
targetFilePaths: readonly string[],
|
|
54
|
+
) => void;
|
|
44
55
|
|
|
45
56
|
constructor(ports: HandlePreToolUseUseCasePorts) {
|
|
46
57
|
this.configQueryPort = ports.configQueryPort;
|
|
47
58
|
this.storyReflectionQueryPort = ports.storyReflectionQueryPort;
|
|
48
59
|
this.fullModeRequirementQueryPort = ports.fullModeRequirementQueryPort;
|
|
60
|
+
this.baselineGrandfatherQueryPort = ports.baselineGrandfatherQueryPort;
|
|
61
|
+
this.grandfatherLogger =
|
|
62
|
+
ports.grandfatherLogger ??
|
|
63
|
+
((reason, paths) =>
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
`[baseline] grandfather skip (${reason}): ${paths.join(', ')}\n`,
|
|
66
|
+
));
|
|
49
67
|
this.translator = new AsyncHookToCliTranslator({
|
|
50
68
|
configQueryPort: ports.configQueryPort,
|
|
51
69
|
reentryGuard: { isActive: () => false } as never,
|
|
@@ -59,6 +77,8 @@ export class HandlePreToolUseUseCase {
|
|
|
59
77
|
throw new HandlePreToolUseInputValidationError('toolNameは必須です(空文字不可)');
|
|
60
78
|
}
|
|
61
79
|
|
|
80
|
+
const grandfather = await this.checkGrandfather(input.targetFilePaths);
|
|
81
|
+
|
|
62
82
|
const hookEvent = HookEvent.createPreToolUse(input.toolName, input.targetFilePaths);
|
|
63
83
|
const result = await this.translator.translate(hookEvent);
|
|
64
84
|
|
|
@@ -66,32 +86,41 @@ export class HandlePreToolUseUseCase {
|
|
|
66
86
|
const metadata = result.blockMetadata;
|
|
67
87
|
const blockedFilePath = metadata?.blockedFilePath ?? input.targetFilePaths[0];
|
|
68
88
|
|
|
69
|
-
if (metadata?.reason === 'PHASE_GATE') {
|
|
70
|
-
return HandlePreToolUseUseCase.buildPhaseGateBlockOutput(blockedFilePath, metadata);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
89
|
if (metadata?.reason === 'PROTECTED_FILE') {
|
|
74
90
|
return HandlePreToolUseUseCase.buildProtectedFileBlockOutput(blockedFilePath);
|
|
75
91
|
}
|
|
76
92
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
93
|
+
if (metadata?.reason === 'PHASE_GATE') {
|
|
94
|
+
if (grandfather.allGrandfathered) {
|
|
95
|
+
this.grandfatherLogger('phase-gate', input.targetFilePaths);
|
|
96
|
+
// fallthrough: continue to full-mode / story-reflection checks (which may also grandfather)
|
|
97
|
+
} else {
|
|
98
|
+
return HandlePreToolUseUseCase.buildPhaseGateBlockOutput(blockedFilePath, metadata);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
return {
|
|
102
|
+
shouldBlock: true,
|
|
103
|
+
blockedFilePath,
|
|
104
|
+
error: {
|
|
105
|
+
message: `ブロックされました: ${blockedFilePath ?? '不明なファイル'}`,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
84
109
|
}
|
|
85
110
|
|
|
86
111
|
if (HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName)
|
|
87
112
|
&& this.fullModeRequirementQueryPort !== undefined
|
|
88
113
|
&& input.targetFilePaths.length > 0) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
114
|
+
if (grandfather.allGrandfathered) {
|
|
115
|
+
this.grandfatherLogger('full-mode', input.targetFilePaths);
|
|
116
|
+
} else {
|
|
117
|
+
const fullModeResult = await this.fullModeRequirementQueryPort.check(input.targetFilePaths);
|
|
118
|
+
if (fullModeResult.requiresFullMode) {
|
|
119
|
+
return HandlePreToolUseUseCase.buildFullModeRequiredBlockOutput(
|
|
120
|
+
input.targetFilePaths[0],
|
|
121
|
+
fullModeResult,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
95
124
|
}
|
|
96
125
|
}
|
|
97
126
|
|
|
@@ -100,6 +129,11 @@ export class HandlePreToolUseUseCase {
|
|
|
100
129
|
return { shouldBlock: false };
|
|
101
130
|
}
|
|
102
131
|
|
|
132
|
+
if (grandfather.allGrandfathered) {
|
|
133
|
+
this.grandfatherLogger('story-reflection', input.targetFilePaths);
|
|
134
|
+
return { shouldBlock: false };
|
|
135
|
+
}
|
|
136
|
+
|
|
103
137
|
const reflectionResult = await this.storyReflectionQueryPort.checkReflection(scope.unitId!);
|
|
104
138
|
|
|
105
139
|
if (reflectionResult.skipped || reflectionResult.passed) {
|
|
@@ -113,6 +147,27 @@ export class HandlePreToolUseUseCase {
|
|
|
113
147
|
);
|
|
114
148
|
}
|
|
115
149
|
|
|
150
|
+
private async checkGrandfather(
|
|
151
|
+
targetFilePaths: readonly string[],
|
|
152
|
+
): Promise<BaselineGrandfatherCheckResult> {
|
|
153
|
+
if (this.baselineGrandfatherQueryPort === undefined) {
|
|
154
|
+
return {
|
|
155
|
+
allGrandfathered: false,
|
|
156
|
+
baselineEnabled: false,
|
|
157
|
+
grandfatheredPaths: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return await this.baselineGrandfatherQueryPort.check(targetFilePaths);
|
|
162
|
+
} catch {
|
|
163
|
+
return {
|
|
164
|
+
allGrandfathered: false,
|
|
165
|
+
baselineEnabled: false,
|
|
166
|
+
grandfatheredPaths: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
116
171
|
private static buildFullModeRequiredBlockOutput(
|
|
117
172
|
blockedFilePath: string | undefined,
|
|
118
173
|
result: {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @unit agent-integration
|
|
2
|
+
// @layer domain
|
|
3
|
+
|
|
4
|
+
export interface BaselineGrandfatherCheckResult {
|
|
5
|
+
readonly allGrandfathered: boolean;
|
|
6
|
+
readonly baselineEnabled: boolean;
|
|
7
|
+
readonly grandfatheredPaths: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BaselineGrandfatherQueryPort {
|
|
11
|
+
check(targetFilePaths: readonly string[]): Promise<BaselineGrandfatherCheckResult>;
|
|
12
|
+
}
|
|
@@ -5,10 +5,16 @@ import type { ProjectPaths } from '../value-objects/project-paths.js';
|
|
|
5
5
|
|
|
6
6
|
export type HookType = 'pre-tool-use' | 'post-tool-use' | 'stop';
|
|
7
7
|
|
|
8
|
+
export interface BaselineConfig {
|
|
9
|
+
readonly enabled: boolean;
|
|
10
|
+
readonly path: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export interface ConfigQueryPort {
|
|
9
14
|
isHookEnabled(hookType: HookType): Promise<boolean>;
|
|
10
15
|
getProtectedFilePatterns(): Promise<string[]>;
|
|
11
16
|
getProtectedFileExclusions(): Promise<string[]>;
|
|
12
17
|
getRelaxedGates(): Promise<readonly string[]>;
|
|
13
18
|
getProjectPaths(): ProjectPaths;
|
|
19
|
+
getBaselineConfig(): Promise<BaselineConfig>;
|
|
14
20
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @unit agent-integration
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
BaselineGrandfatherQueryPort,
|
|
6
|
+
BaselineGrandfatherCheckResult,
|
|
7
|
+
} from '../../domain/ports/baseline-grandfather-query-port.js';
|
|
8
|
+
import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
|
|
9
|
+
import type { BaselineRepositoryPort } from '../../../ci-governance/domain/ports/baseline-repository-port.js';
|
|
10
|
+
import { BaselineJsonRepositoryAdapter } from '../../../ci-governance/infrastructure/adapters/baseline-json-repository-adapter.js';
|
|
11
|
+
|
|
12
|
+
export interface CiGovernanceBaselineGrandfatherAdapterDeps {
|
|
13
|
+
readonly baseDir: string;
|
|
14
|
+
readonly configQueryPort: ConfigQueryPort;
|
|
15
|
+
readonly baselineRepositoryFactory?: (
|
|
16
|
+
baseDir: string,
|
|
17
|
+
relativePath: string,
|
|
18
|
+
) => BaselineRepositoryPort;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class CiGovernanceBaselineGrandfatherAdapter
|
|
22
|
+
implements BaselineGrandfatherQueryPort
|
|
23
|
+
{
|
|
24
|
+
constructor(private readonly deps: CiGovernanceBaselineGrandfatherAdapterDeps) {}
|
|
25
|
+
|
|
26
|
+
async check(
|
|
27
|
+
targetFilePaths: readonly string[],
|
|
28
|
+
): Promise<BaselineGrandfatherCheckResult> {
|
|
29
|
+
try {
|
|
30
|
+
const baselineConfig = await this.deps.configQueryPort.getBaselineConfig();
|
|
31
|
+
|
|
32
|
+
if (!baselineConfig.enabled) {
|
|
33
|
+
return {
|
|
34
|
+
allGrandfathered: false,
|
|
35
|
+
baselineEnabled: false,
|
|
36
|
+
grandfatheredPaths: [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (targetFilePaths.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
allGrandfathered: false,
|
|
43
|
+
baselineEnabled: true,
|
|
44
|
+
grandfatheredPaths: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const factory =
|
|
49
|
+
this.deps.baselineRepositoryFactory ??
|
|
50
|
+
((baseDir, relativePath) =>
|
|
51
|
+
new BaselineJsonRepositoryAdapter(baseDir, relativePath));
|
|
52
|
+
const repository = factory(this.deps.baseDir, baselineConfig.path);
|
|
53
|
+
|
|
54
|
+
if (!(await repository.exists())) {
|
|
55
|
+
return {
|
|
56
|
+
allGrandfathered: false,
|
|
57
|
+
baselineEnabled: true,
|
|
58
|
+
grandfatheredPaths: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const snapshot = await repository.load();
|
|
63
|
+
if (snapshot === null) {
|
|
64
|
+
return {
|
|
65
|
+
allGrandfathered: false,
|
|
66
|
+
baselineEnabled: true,
|
|
67
|
+
grandfatheredPaths: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const grandfathered: string[] = [];
|
|
72
|
+
for (const p of targetFilePaths) {
|
|
73
|
+
if (snapshot.contains(p)) grandfathered.push(p);
|
|
74
|
+
}
|
|
75
|
+
const allGrandfathered = grandfathered.length === targetFilePaths.length;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
allGrandfathered,
|
|
79
|
+
baselineEnabled: true,
|
|
80
|
+
grandfatheredPaths: grandfathered,
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return {
|
|
84
|
+
allGrandfathered: false,
|
|
85
|
+
baselineEnabled: false,
|
|
86
|
+
grandfatheredPaths: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import * as fs from 'node:fs';
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
BaselineConfig,
|
|
12
|
+
ConfigQueryPort,
|
|
13
|
+
HookType,
|
|
14
|
+
} from '../../domain/ports/config-query-port.js';
|
|
11
15
|
import { ProjectPaths } from '../../domain/value-objects/project-paths.js';
|
|
12
16
|
|
|
13
17
|
interface ProjectDocsSection {
|
|
@@ -39,11 +43,17 @@ interface QuickModeSection {
|
|
|
39
43
|
relaxedGates?: string[];
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
interface BaselineSection {
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
path?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
interface HarnessConfigDocument {
|
|
43
52
|
harnesses?: HarnessesSection;
|
|
44
53
|
project?: ProjectSection;
|
|
45
54
|
protectedFiles?: ProtectedFilesSection;
|
|
46
55
|
quickMode?: QuickModeSection;
|
|
56
|
+
baseline?: BaselineSection;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
@@ -109,4 +119,13 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
109
119
|
},
|
|
110
120
|
);
|
|
111
121
|
}
|
|
122
|
+
|
|
123
|
+
async getBaselineConfig(): Promise<BaselineConfig> {
|
|
124
|
+
const config = this.loadConfig();
|
|
125
|
+
const baseline = config.baseline ?? {};
|
|
126
|
+
return {
|
|
127
|
+
enabled: baseline.enabled ?? false,
|
|
128
|
+
path: baseline.path ?? '.phasegate/baseline.json',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
112
131
|
}
|
|
@@ -13,6 +13,7 @@ import { HarnessConfigConfigQueryAdapter } from '../infrastructure/adapters/harn
|
|
|
13
13
|
import { PhaseGateQueryAdapter } from '../infrastructure/adapters/phase-gate-query-adapter.js';
|
|
14
14
|
import { FileSystemStoryReflectionQueryAdapter } from '../infrastructure/adapters/file-system-story-reflection-query-adapter.js';
|
|
15
15
|
import { QuickModeFullModeRequirementAdapter } from '../infrastructure/adapters/quick-mode-full-mode-requirement-adapter.js';
|
|
16
|
+
import { CiGovernanceBaselineGrandfatherAdapter } from '../infrastructure/adapters/ci-governance-baseline-grandfather-adapter.js';
|
|
16
17
|
import { createQuickModeCompositionRoot } from '../../quick-mode/composition-root.js';
|
|
17
18
|
import * as path from 'node:path';
|
|
18
19
|
import * as fs from 'node:fs/promises';
|
|
@@ -123,11 +124,16 @@ async function main(): Promise<void> {
|
|
|
123
124
|
const fullModeRequirementQueryPort = new QuickModeFullModeRequirementAdapter({
|
|
124
125
|
classifyUseCaseFactory: () => createQuickModeCompositionRoot().classifyUseCase,
|
|
125
126
|
});
|
|
127
|
+
const baselineGrandfatherQueryPort = new CiGovernanceBaselineGrandfatherAdapter({
|
|
128
|
+
baseDir: path.dirname(configPath),
|
|
129
|
+
configQueryPort,
|
|
130
|
+
});
|
|
126
131
|
const useCase = new HandlePreToolUseUseCase({
|
|
127
132
|
configQueryPort,
|
|
128
133
|
phaseGateQueryPort,
|
|
129
134
|
storyReflectionQueryPort,
|
|
130
135
|
fullModeRequirementQueryPort,
|
|
136
|
+
baselineGrandfatherQueryPort,
|
|
131
137
|
});
|
|
132
138
|
|
|
133
139
|
const output = await useCase.execute({ toolName: effectiveToolName, targetFilePaths });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
export interface CreateBaselineOutputEntry {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly sha1: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CreateBaselineOutput {
|
|
10
|
+
readonly savedPath: string;
|
|
11
|
+
readonly entryCount: number;
|
|
12
|
+
readonly dryRun: boolean;
|
|
13
|
+
readonly overwriteBlocked: boolean;
|
|
14
|
+
readonly entries: readonly CreateBaselineOutputEntry[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer application
|
|
3
|
+
|
|
4
|
+
import type { FileScannerPort } from '../../domain/ports/file-scanner-port.js';
|
|
5
|
+
import type { FileHasherPort } from '../../domain/ports/file-hasher-port.js';
|
|
6
|
+
import type { BaselineRepositoryPort } from '../../domain/ports/baseline-repository-port.js';
|
|
7
|
+
import { BaselineEntry } from '../../domain/value-objects/baseline-entry.js';
|
|
8
|
+
import { BaselineSnapshot } from '../../domain/value-objects/baseline-snapshot.js';
|
|
9
|
+
import type { CreateBaselineInput } from '../dto/create-baseline-input.js';
|
|
10
|
+
import type { CreateBaselineOutput } from '../dto/create-baseline-output.js';
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_BASELINE_INCLUDE: readonly string[] = Object.freeze([
|
|
13
|
+
'scripts/**/*.ts',
|
|
14
|
+
'src/**/*.ts',
|
|
15
|
+
'src/**/*.tsx',
|
|
16
|
+
'src/**/*.js',
|
|
17
|
+
'src/**/*.jsx',
|
|
18
|
+
'docs/product/construction/**/*.md',
|
|
19
|
+
'docs/inception/**/*.md',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_BASELINE_EXCLUDE: readonly string[] = Object.freeze([
|
|
23
|
+
'node_modules/**',
|
|
24
|
+
'dist/**',
|
|
25
|
+
'build/**',
|
|
26
|
+
'.next/**',
|
|
27
|
+
'coverage/**',
|
|
28
|
+
'**/__tests__/**',
|
|
29
|
+
'**/*.test.ts',
|
|
30
|
+
'**/*.spec.ts',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export class CreateBaselineUseCase {
|
|
34
|
+
constructor(
|
|
35
|
+
private readonly scanner: FileScannerPort,
|
|
36
|
+
private readonly hasher: FileHasherPort,
|
|
37
|
+
private readonly repository: BaselineRepositoryPort,
|
|
38
|
+
private readonly clock: () => Date = () => new Date(),
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
async execute(input: CreateBaselineInput = {}): Promise<CreateBaselineOutput> {
|
|
42
|
+
const include = input.include ?? DEFAULT_BASELINE_INCLUDE;
|
|
43
|
+
const exclude = input.exclude ?? DEFAULT_BASELINE_EXCLUDE;
|
|
44
|
+
const dryRun = input.dryRun === true;
|
|
45
|
+
const force = input.force === true;
|
|
46
|
+
|
|
47
|
+
const savedPath = this.repository.getPath();
|
|
48
|
+
|
|
49
|
+
if (!dryRun && !force && (await this.repository.exists())) {
|
|
50
|
+
return {
|
|
51
|
+
savedPath,
|
|
52
|
+
entryCount: 0,
|
|
53
|
+
dryRun: false,
|
|
54
|
+
overwriteBlocked: true,
|
|
55
|
+
entries: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const scanned = await this.scanner.scan({ include, exclude });
|
|
60
|
+
const sortedPaths = [...scanned].sort();
|
|
61
|
+
|
|
62
|
+
const entries: BaselineEntry[] = [];
|
|
63
|
+
for (const p of sortedPaths) {
|
|
64
|
+
const sha1 = await this.hasher.hashFile(p);
|
|
65
|
+
entries.push(BaselineEntry.create({ path: p, sha1 }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const snapshot = BaselineSnapshot.create({
|
|
69
|
+
createdAt: this.clock().toISOString(),
|
|
70
|
+
algorithm: 'sha1',
|
|
71
|
+
entries,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const outputEntries = entries.map((e) => ({ path: e.path, sha1: e.sha1 }));
|
|
75
|
+
|
|
76
|
+
if (dryRun) {
|
|
77
|
+
return {
|
|
78
|
+
savedPath,
|
|
79
|
+
entryCount: snapshot.entryCount,
|
|
80
|
+
dryRun: true,
|
|
81
|
+
overwriteBlocked: false,
|
|
82
|
+
entries: outputEntries,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const writtenPath = await this.repository.save(snapshot);
|
|
87
|
+
return {
|
|
88
|
+
savedPath: writtenPath,
|
|
89
|
+
entryCount: snapshot.entryCount,
|
|
90
|
+
dryRun: false,
|
|
91
|
+
overwriteBlocked: false,
|
|
92
|
+
entries: outputEntries,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -33,17 +33,25 @@ import { AdrFoundationExistenceAdapter } from './infrastructure/adapters/adr-fou
|
|
|
33
33
|
import { GenerateCiTemplateHandler } from './presentation/handlers/generate-ci-template-handler.js';
|
|
34
34
|
import { MigrateAgentsMdHandler } from './presentation/handlers/migrate-agents-md-handler.js';
|
|
35
35
|
import { CheckRepetitionHandler } from './presentation/handlers/check-repetition-handler.js';
|
|
36
|
+
import { CreateBaselineHandler } from './presentation/handlers/create-baseline-handler.js';
|
|
37
|
+
|
|
38
|
+
import { CreateBaselineUseCase } from './application/usecases/create-baseline-usecase.js';
|
|
39
|
+
import { GlobFileScannerAdapter } from './infrastructure/adapters/glob-file-scanner-adapter.js';
|
|
40
|
+
import { FileSystemSha1HasherAdapter } from './infrastructure/adapters/file-system-sha1-hasher-adapter.js';
|
|
41
|
+
import { BaselineJsonRepositoryAdapter } from './infrastructure/adapters/baseline-json-repository-adapter.js';
|
|
36
42
|
|
|
37
43
|
export interface CiGovernanceCompositionRoot {
|
|
38
44
|
generateCiTemplateHandler: GenerateCiTemplateHandler;
|
|
39
45
|
migrateAgentsMdHandler: MigrateAgentsMdHandler;
|
|
40
46
|
checkRepetitionHandler: CheckRepetitionHandler;
|
|
47
|
+
createBaselineHandler: CreateBaselineHandler;
|
|
41
48
|
// Use cases exposed for direct access
|
|
42
49
|
recordErrorOccurrenceUseCase: RecordErrorOccurrenceUseCase;
|
|
43
50
|
checkEscalationUseCase: CheckEscalationUseCase;
|
|
44
51
|
resetRepetitionUseCase: ResetRepetitionUseCase;
|
|
45
52
|
aggregateLessonsUseCase: AggregateLessonsUseCase;
|
|
46
53
|
validatePointersUseCase: ValidatePointersUseCase;
|
|
54
|
+
createBaselineUseCase: CreateBaselineUseCase;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
export function buildCiGovernance(baseDir: string): CiGovernanceCompositionRoot {
|
|
@@ -80,6 +88,16 @@ export function buildCiGovernance(baseDir: string): CiGovernanceCompositionRoot
|
|
|
80
88
|
const aggregateLessonsUseCase = new AggregateLessonsUseCase(lessonArtifactReaderPort, lessonAggregator);
|
|
81
89
|
const validatePointersUseCase = new ValidatePointersUseCase(agentsMdPort, pointerValidator);
|
|
82
90
|
|
|
91
|
+
// Baseline adapters & use case (ISSUE-007 Wave 1)
|
|
92
|
+
const fileScanner = new GlobFileScannerAdapter(baseDir);
|
|
93
|
+
const fileHasher = new FileSystemSha1HasherAdapter(baseDir);
|
|
94
|
+
const baselineRepository = new BaselineJsonRepositoryAdapter(baseDir);
|
|
95
|
+
const createBaselineUseCase = new CreateBaselineUseCase(
|
|
96
|
+
fileScanner,
|
|
97
|
+
fileHasher,
|
|
98
|
+
baselineRepository,
|
|
99
|
+
);
|
|
100
|
+
|
|
83
101
|
// Handlers
|
|
84
102
|
const generateCiTemplateHandler = new GenerateCiTemplateHandler(
|
|
85
103
|
generateCiTemplateUseCase,
|
|
@@ -87,15 +105,18 @@ export function buildCiGovernance(baseDir: string): CiGovernanceCompositionRoot
|
|
|
87
105
|
);
|
|
88
106
|
const migrateAgentsMdHandler = new MigrateAgentsMdHandler(migrateAgentsMdUseCase);
|
|
89
107
|
const checkRepetitionHandler = new CheckRepetitionHandler(checkEscalationUseCase);
|
|
108
|
+
const createBaselineHandler = new CreateBaselineHandler(createBaselineUseCase);
|
|
90
109
|
|
|
91
110
|
return {
|
|
92
111
|
generateCiTemplateHandler,
|
|
93
112
|
migrateAgentsMdHandler,
|
|
94
113
|
checkRepetitionHandler,
|
|
114
|
+
createBaselineHandler,
|
|
95
115
|
recordErrorOccurrenceUseCase,
|
|
96
116
|
checkEscalationUseCase,
|
|
97
117
|
resetRepetitionUseCase,
|
|
98
118
|
aggregateLessonsUseCase,
|
|
99
119
|
validatePointersUseCase,
|
|
120
|
+
createBaselineUseCase,
|
|
100
121
|
};
|
|
101
122
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
|
|
4
|
+
import type { BaselineSnapshot } from '../value-objects/baseline-snapshot.js';
|
|
5
|
+
|
|
6
|
+
export interface BaselineRepositoryPort {
|
|
7
|
+
save(snapshot: BaselineSnapshot): Promise<string>;
|
|
8
|
+
load(): Promise<BaselineSnapshot | null>;
|
|
9
|
+
exists(): Promise<boolean>;
|
|
10
|
+
getPath(): string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
|
|
4
|
+
export interface FileScanOptions {
|
|
5
|
+
readonly include: readonly string[];
|
|
6
|
+
readonly exclude: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FileScannerPort {
|
|
10
|
+
scan(options: FileScanOptions): Promise<readonly string[]>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
|
|
4
|
+
export interface BaselineEntryProps {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly sha1: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class BaselineEntry {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly sha1: string;
|
|
12
|
+
|
|
13
|
+
private constructor(props: BaselineEntryProps) {
|
|
14
|
+
this.path = props.path;
|
|
15
|
+
this.sha1 = props.sha1;
|
|
16
|
+
Object.freeze(this);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static create(props: BaselineEntryProps): BaselineEntry {
|
|
20
|
+
if (props.path.length === 0) {
|
|
21
|
+
throw new Error('BaselineEntry: path must not be empty');
|
|
22
|
+
}
|
|
23
|
+
if (!/^[0-9a-f]{40}$/.test(props.sha1)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`BaselineEntry: sha1 must be 40 lowercase hex chars, got: ${props.sha1}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return new BaselineEntry(props);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
equals(other: BaselineEntry): boolean {
|
|
32
|
+
return this.path === other.path && this.sha1 === other.sha1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
|
|
4
|
+
import { BaselineEntry } from './baseline-entry.js';
|
|
5
|
+
|
|
6
|
+
export interface BaselineSnapshotProps {
|
|
7
|
+
readonly createdAt: string;
|
|
8
|
+
readonly algorithm: 'sha1';
|
|
9
|
+
readonly entries: readonly BaselineEntry[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ISO_8601_UTC_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
|
13
|
+
|
|
14
|
+
export class BaselineSnapshot {
|
|
15
|
+
readonly createdAt: string;
|
|
16
|
+
readonly algorithm: 'sha1';
|
|
17
|
+
readonly entries: readonly BaselineEntry[];
|
|
18
|
+
private readonly pathIndex: ReadonlySet<string>;
|
|
19
|
+
|
|
20
|
+
private constructor(props: BaselineSnapshotProps) {
|
|
21
|
+
this.createdAt = props.createdAt;
|
|
22
|
+
this.algorithm = props.algorithm;
|
|
23
|
+
this.entries = Object.freeze([...props.entries]);
|
|
24
|
+
this.pathIndex = new Set(props.entries.map((e) => e.path));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static create(props: BaselineSnapshotProps): BaselineSnapshot {
|
|
28
|
+
if (!ISO_8601_UTC_RE.test(props.createdAt)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`BaselineSnapshot: createdAt must be ISO 8601 UTC, got: ${props.createdAt}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (props.algorithm !== 'sha1') {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`BaselineSnapshot: algorithm must be 'sha1', got: ${props.algorithm}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const seen = new Set<string>();
|
|
39
|
+
for (const e of props.entries) {
|
|
40
|
+
if (seen.has(e.path)) {
|
|
41
|
+
throw new Error(`BaselineSnapshot: duplicate path ${e.path}`);
|
|
42
|
+
}
|
|
43
|
+
seen.add(e.path);
|
|
44
|
+
}
|
|
45
|
+
return new BaselineSnapshot(props);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
contains(path: string): boolean {
|
|
49
|
+
return this.pathIndex.has(path);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get entryCount(): number {
|
|
53
|
+
return this.entries.length;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/scripts/harness/ci-governance/infrastructure/adapters/baseline-json-repository-adapter.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import type { BaselineRepositoryPort } from '../../domain/ports/baseline-repository-port.js';
|
|
7
|
+
import { BaselineSnapshot } from '../../domain/value-objects/baseline-snapshot.js';
|
|
8
|
+
import { BaselineEntry } from '../../domain/value-objects/baseline-entry.js';
|
|
9
|
+
|
|
10
|
+
interface BaselineJsonV1 {
|
|
11
|
+
readonly version: '1.0';
|
|
12
|
+
readonly createdAt: string;
|
|
13
|
+
readonly algorithm: 'sha1';
|
|
14
|
+
readonly files: ReadonlyArray<{ readonly path: string; readonly sha1: string }>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class BaselineJsonRepositoryAdapter implements BaselineRepositoryPort {
|
|
18
|
+
private readonly filePath: string;
|
|
19
|
+
|
|
20
|
+
constructor(baseDir: string, relativePath = '.phasegate/baseline.json') {
|
|
21
|
+
this.filePath = path.isAbsolute(relativePath)
|
|
22
|
+
? relativePath
|
|
23
|
+
: path.join(baseDir, relativePath);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getPath(): string {
|
|
27
|
+
return this.filePath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async exists(): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(this.filePath);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async save(snapshot: BaselineSnapshot): Promise<string> {
|
|
40
|
+
const data: BaselineJsonV1 = {
|
|
41
|
+
version: '1.0',
|
|
42
|
+
createdAt: snapshot.createdAt,
|
|
43
|
+
algorithm: snapshot.algorithm,
|
|
44
|
+
files: snapshot.entries.map((e) => ({ path: e.path, sha1: e.sha1 })),
|
|
45
|
+
};
|
|
46
|
+
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
|
|
47
|
+
await fs.writeFile(
|
|
48
|
+
this.filePath,
|
|
49
|
+
`${JSON.stringify(data, null, 2)}\n`,
|
|
50
|
+
'utf-8',
|
|
51
|
+
);
|
|
52
|
+
return this.filePath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async load(): Promise<BaselineSnapshot | null> {
|
|
56
|
+
let content: string;
|
|
57
|
+
try {
|
|
58
|
+
content = await fs.readFile(this.filePath, 'utf-8');
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let parsed: BaselineJsonV1;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(content) as BaselineJsonV1;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Invalid baseline JSON at ${this.filePath}: ${String(err)}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
!parsed ||
|
|
75
|
+
parsed.version !== '1.0' ||
|
|
76
|
+
parsed.algorithm !== 'sha1' ||
|
|
77
|
+
!Array.isArray(parsed.files)
|
|
78
|
+
) {
|
|
79
|
+
throw new Error(`Invalid baseline JSON schema at ${this.filePath}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const entries = parsed.files.map((f) =>
|
|
83
|
+
BaselineEntry.create({ path: f.path, sha1: f.sha1 }),
|
|
84
|
+
);
|
|
85
|
+
return BaselineSnapshot.create({
|
|
86
|
+
createdAt: parsed.createdAt,
|
|
87
|
+
algorithm: parsed.algorithm,
|
|
88
|
+
entries,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
package/scripts/harness/ci-governance/infrastructure/adapters/file-system-sha1-hasher-adapter.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import * as crypto from 'node:crypto';
|
|
7
|
+
import type { FileHasherPort } from '../../domain/ports/file-hasher-port.js';
|
|
8
|
+
|
|
9
|
+
export class FileSystemSha1HasherAdapter implements FileHasherPort {
|
|
10
|
+
constructor(private readonly baseDir: string) {}
|
|
11
|
+
|
|
12
|
+
async hashFile(relativePath: string): Promise<string> {
|
|
13
|
+
const absPath = path.isAbsolute(relativePath)
|
|
14
|
+
? relativePath
|
|
15
|
+
: path.join(this.baseDir, relativePath);
|
|
16
|
+
const content = await fs.readFile(absPath);
|
|
17
|
+
return crypto.createHash('sha1').update(content).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
|
|
4
|
+
import * as fs from 'node:fs/promises';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import type {
|
|
8
|
+
FileScannerPort,
|
|
9
|
+
FileScanOptions,
|
|
10
|
+
} from '../../domain/ports/file-scanner-port.js';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const picomatch = require('picomatch') as (
|
|
14
|
+
pattern: string,
|
|
15
|
+
options?: { dot?: boolean },
|
|
16
|
+
) => (p: string) => boolean;
|
|
17
|
+
|
|
18
|
+
const HARD_SKIP_DIRS: ReadonlySet<string> = new Set([
|
|
19
|
+
'node_modules',
|
|
20
|
+
'.git',
|
|
21
|
+
'dist',
|
|
22
|
+
'build',
|
|
23
|
+
'.next',
|
|
24
|
+
'coverage',
|
|
25
|
+
'.phasegate',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export class GlobFileScannerAdapter implements FileScannerPort {
|
|
29
|
+
constructor(private readonly baseDir: string) {}
|
|
30
|
+
|
|
31
|
+
async scan(options: FileScanOptions): Promise<readonly string[]> {
|
|
32
|
+
const includeMatchers = options.include.map((p) =>
|
|
33
|
+
picomatch(p, { dot: true }),
|
|
34
|
+
);
|
|
35
|
+
const excludeMatchers = options.exclude.map((p) =>
|
|
36
|
+
picomatch(p, { dot: true }),
|
|
37
|
+
);
|
|
38
|
+
const collected: string[] = [];
|
|
39
|
+
await this.walk(this.baseDir, '', includeMatchers, excludeMatchers, collected);
|
|
40
|
+
return collected.sort();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async walk(
|
|
44
|
+
absDir: string,
|
|
45
|
+
relDir: string,
|
|
46
|
+
includeMatchers: Array<(p: string) => boolean>,
|
|
47
|
+
excludeMatchers: Array<(p: string) => boolean>,
|
|
48
|
+
out: string[],
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
let entries: Array<{
|
|
51
|
+
name: string;
|
|
52
|
+
isDirectory: () => boolean;
|
|
53
|
+
isFile: () => boolean;
|
|
54
|
+
}>;
|
|
55
|
+
try {
|
|
56
|
+
entries = (await fs.readdir(absDir, {
|
|
57
|
+
withFileTypes: true,
|
|
58
|
+
})) as unknown as typeof entries;
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const ent of entries) {
|
|
64
|
+
if (ent.isDirectory() && HARD_SKIP_DIRS.has(ent.name)) continue;
|
|
65
|
+
const relPath = relDir === '' ? ent.name : `${relDir}/${ent.name}`;
|
|
66
|
+
const absPath = path.join(absDir, ent.name);
|
|
67
|
+
|
|
68
|
+
if (ent.isDirectory()) {
|
|
69
|
+
await this.walk(absPath, relPath, includeMatchers, excludeMatchers, out);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!ent.isFile()) continue;
|
|
74
|
+
if (!includeMatchers.some((m) => m(relPath))) continue;
|
|
75
|
+
if (excludeMatchers.some((m) => m(relPath))) continue;
|
|
76
|
+
out.push(relPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer presentation
|
|
3
|
+
|
|
4
|
+
import type { CreateBaselineUseCase } from '../../application/usecases/create-baseline-usecase.js';
|
|
5
|
+
|
|
6
|
+
export interface CreateBaselineHandlerArgs {
|
|
7
|
+
readonly include?: readonly string[];
|
|
8
|
+
readonly exclude?: readonly string[];
|
|
9
|
+
readonly dryRun?: boolean;
|
|
10
|
+
readonly force?: boolean;
|
|
11
|
+
readonly format?: 'human' | 'json';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CreateBaselineHandlerResult {
|
|
15
|
+
readonly exitCode: number;
|
|
16
|
+
readonly output: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class CreateBaselineHandler {
|
|
20
|
+
constructor(private readonly useCase: CreateBaselineUseCase) {}
|
|
21
|
+
|
|
22
|
+
async handle(args: CreateBaselineHandlerArgs): Promise<CreateBaselineHandlerResult> {
|
|
23
|
+
const format = args.format ?? 'human';
|
|
24
|
+
const result = await this.useCase.execute({
|
|
25
|
+
include: args.include,
|
|
26
|
+
exclude: args.exclude,
|
|
27
|
+
dryRun: args.dryRun,
|
|
28
|
+
force: args.force,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (format === 'json') {
|
|
32
|
+
return {
|
|
33
|
+
exitCode: result.overwriteBlocked ? 2 : 0,
|
|
34
|
+
output: JSON.stringify(result, null, 2),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (result.overwriteBlocked) {
|
|
39
|
+
return {
|
|
40
|
+
exitCode: 2,
|
|
41
|
+
output: [
|
|
42
|
+
`baseline は既に存在します: ${result.savedPath}`,
|
|
43
|
+
'上書きするには --force を指定してください。',
|
|
44
|
+
'生成内容のみを確認したい場合は --dry-run を指定してください。',
|
|
45
|
+
].join('\n'),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const header = result.dryRun
|
|
50
|
+
? `[dry run] baseline を生成しました(保存先想定: ${result.savedPath})`
|
|
51
|
+
: `baseline を保存しました: ${result.savedPath}`;
|
|
52
|
+
return {
|
|
53
|
+
exitCode: 0,
|
|
54
|
+
output: [header, `エントリ数: ${result.entryCount}`].join('\n'),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json
CHANGED
|
@@ -439,6 +439,19 @@
|
|
|
439
439
|
"uniqueItems": true
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
|
+
},
|
|
443
|
+
"baseline": {
|
|
444
|
+
"type": "object",
|
|
445
|
+
"additionalProperties": false,
|
|
446
|
+
"properties": {
|
|
447
|
+
"enabled": {
|
|
448
|
+
"type": "boolean"
|
|
449
|
+
},
|
|
450
|
+
"path": {
|
|
451
|
+
"type": "string",
|
|
452
|
+
"minLength": 1
|
|
453
|
+
}
|
|
454
|
+
}
|
|
442
455
|
}
|
|
443
456
|
}
|
|
444
457
|
}
|
package/scripts/harness/main.ts
CHANGED
|
@@ -88,6 +88,7 @@ Commands:
|
|
|
88
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
|
+
baseline Create retrofit baseline snapshot (--dry-run, --force, --paths <glob,glob,...>, --json)
|
|
91
92
|
|
|
92
93
|
skill:execute-tdd-cycle Execute TDD cycle (--unit, --story, --desc, --phase RED|GREEN|REFACTOR, --passed)
|
|
93
94
|
skill:check-coverage Check coverage (--story <storyId>, --json)
|
|
@@ -896,6 +897,26 @@ Examples:
|
|
|
896
897
|
break;
|
|
897
898
|
}
|
|
898
899
|
|
|
900
|
+
case 'baseline': {
|
|
901
|
+
const mod = buildCiGovernance(rootDir);
|
|
902
|
+
const dryRun = hasFlag(args, '--dry-run');
|
|
903
|
+
const force = hasFlag(args, '--force');
|
|
904
|
+
const pathsFlag = parseFlag(args, '--paths');
|
|
905
|
+
const include = pathsFlag
|
|
906
|
+
? pathsFlag.split(',').map((s) => s.trim()).filter(Boolean)
|
|
907
|
+
: undefined;
|
|
908
|
+
const format = json ? 'json' : 'human';
|
|
909
|
+
const result = await mod.createBaselineHandler.handle({
|
|
910
|
+
include,
|
|
911
|
+
dryRun,
|
|
912
|
+
force,
|
|
913
|
+
format,
|
|
914
|
+
});
|
|
915
|
+
console.log(result.output);
|
|
916
|
+
process.exit(result.exitCode);
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
|
|
899
920
|
// ── skill-quality ──
|
|
900
921
|
case 'skill:execute-tdd-cycle': {
|
|
901
922
|
const mod = createSkillQualityHandlers();
|