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.
Files changed (22) hide show
  1. package/package.json +1 -1
  2. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +72 -17
  3. package/scripts/harness/agent-integration/domain/ports/baseline-grandfather-query-port.ts +12 -0
  4. package/scripts/harness/agent-integration/domain/ports/config-query-port.ts +6 -0
  5. package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +90 -0
  6. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +20 -1
  7. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +6 -0
  8. package/scripts/harness/ci-governance/application/dto/create-baseline-input.ts +9 -0
  9. package/scripts/harness/ci-governance/application/dto/create-baseline-output.ts +15 -0
  10. package/scripts/harness/ci-governance/application/usecases/create-baseline-usecase.ts +95 -0
  11. package/scripts/harness/ci-governance/composition-root.ts +21 -0
  12. package/scripts/harness/ci-governance/domain/ports/baseline-repository-port.ts +11 -0
  13. package/scripts/harness/ci-governance/domain/ports/file-hasher-port.ts +6 -0
  14. package/scripts/harness/ci-governance/domain/ports/file-scanner-port.ts +11 -0
  15. package/scripts/harness/ci-governance/domain/value-objects/baseline-entry.ts +34 -0
  16. package/scripts/harness/ci-governance/domain/value-objects/baseline-snapshot.ts +55 -0
  17. package/scripts/harness/ci-governance/infrastructure/adapters/baseline-json-repository-adapter.ts +91 -0
  18. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-sha1-hasher-adapter.ts +19 -0
  19. package/scripts/harness/ci-governance/infrastructure/adapters/glob-file-scanner-adapter.ts +79 -0
  20. package/scripts/harness/ci-governance/presentation/handlers/create-baseline-handler.ts +57 -0
  21. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +13 -0
  22. package/scripts/harness/main.ts +21 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.64.0",
3
+ "version": "0.66.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "Apache-2.0",
@@ -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
- return {
78
- shouldBlock: true,
79
- blockedFilePath,
80
- error: {
81
- message: `ブロックされました: ${blockedFilePath ?? '不明なファイル'}`,
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
- const fullModeResult = await this.fullModeRequirementQueryPort.check(input.targetFilePaths);
90
- if (fullModeResult.requiresFullMode) {
91
- return HandlePreToolUseUseCase.buildFullModeRequiredBlockOutput(
92
- input.targetFilePaths[0],
93
- fullModeResult,
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 { ConfigQueryPort, HookType } from '../../domain/ports/config-query-port.js';
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,9 @@
1
+ // @unit ci-governance
2
+ // @layer application
3
+
4
+ export interface CreateBaselineInput {
5
+ readonly include?: readonly string[];
6
+ readonly exclude?: readonly string[];
7
+ readonly dryRun?: boolean;
8
+ readonly force?: boolean;
9
+ }
@@ -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,6 @@
1
+ // @unit ci-governance
2
+ // @layer domain
3
+
4
+ export interface FileHasherPort {
5
+ hashFile(relativePath: string): Promise<string>;
6
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
  }
@@ -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();