phasegate 0.65.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.65.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 });