phasegate 0.91.0 → 0.108.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/CHANGELOG.md +255 -0
- package/README.ja.md +17 -7
- package/README.md +26 -4
- package/docs/ADR/ADR-015-architecture-preset.md +183 -0
- package/docs/guide/cli-reference.md +108 -0
- package/docs/guide/codex-integration.md +7 -2
- package/docs/guide/installation.md +10 -2
- package/docs/guide/preset-selection.md +170 -0
- package/docs/guide/quick-vs-full-mode.md +3 -3
- package/docs/guide/retrofit-adoption.md +19 -2
- package/docs/guide/skills-overview.md +1 -1
- package/package.json +7 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -100
- package/scripts/harness/agent-integration/domain/ports/phase-gate-query-port.ts +3 -3
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +26 -30
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +6 -2
- package/scripts/harness/biome-ast-engine/application/dto/analyze-import-graph-input.ts +3 -0
- package/scripts/harness/biome-ast-engine/application/dto/resolve-enabled-rules-output.ts +2 -0
- package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts +4 -1
- package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts +4 -1
- package/scripts/harness/biome-ast-engine/application/usecases/execute-lint-usecase.ts +2 -0
- package/scripts/harness/biome-ast-engine/application/usecases/resolve-enabled-rules-usecase.ts +31 -3
- package/scripts/harness/biome-ast-engine/composition-root.ts +10 -2
- package/scripts/harness/biome-ast-engine/domain/ports/rule-config-provider-port.ts +16 -0
- package/scripts/harness/biome-ast-engine/domain/ports/source-module-analyzer-port.ts +5 -1
- package/scripts/harness/biome-ast-engine/domain/services/lint-runner.ts +5 -1
- package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +38 -0
- package/scripts/harness/biome-ast-engine/domain/value-objects/layer-boundary.ts +7 -8
- package/scripts/harness/biome-ast-engine/domain/value-objects/layer-name.ts +15 -23
- package/scripts/harness/biome-ast-engine/domain/value-objects/source-module-snapshot.ts +11 -4
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-config-provider-adapter.ts +29 -3
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +22 -15
- package/scripts/harness/biome-ast-engine/infrastructure/mappers/source-module-snapshot-mapper.ts +26 -17
- package/scripts/harness/config-foundation/application/dto/resolved-config-output.ts +1 -0
- package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts +34 -2
- package/scripts/harness/config-foundation/application/usecases/migrate-schema-use-case.ts +89 -0
- package/scripts/harness/config-foundation/composition-root.ts +8 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +6 -0
- package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +257 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +66 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +546 -0
- package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +18 -6
- package/scripts/harness/config-foundation/presentation/cli/migrate-schema-command-handler.ts +83 -0
- package/scripts/harness/integrations/pre-commit.ts +211 -52
- package/scripts/harness/main.ts +460 -340
- package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -4
- package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +54 -17
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +154 -36
- package/scripts/harness/setup/skill-deployer.ts +140 -102
- package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +24 -23
- package/scripts/harness/skill-quality/domain/value-objects/commit-message.ts +23 -7
- package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts +52 -0
- package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts +29 -0
- package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +83 -18
- package/scripts/harness/traceability-model/composition-root.ts +48 -30
- package/scripts/harness/traceability-model/domain/ports/design-document-port.ts +9 -15
- package/scripts/harness/traceability-model/domain/ports/work-item-migration-apply-port.ts +11 -0
- package/scripts/harness/traceability-model/domain/ports/work-item-migration-source-port.ts +9 -0
- package/scripts/harness/traceability-model/domain/services/work-item-migration-planner.ts +162 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +57 -0
- package/scripts/harness/traceability-model/domain/value-objects/work-item-migration-candidate.ts +47 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-apply-gateway.ts +110 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-source-gateway.ts +182 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +29 -43
- package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +136 -0
- package/scripts/harness/traceability-model/presentation/cli/migrate-work-items-command-handler.ts +186 -0
- package/skills/quick-implementor/SKILL.md +17 -1
- package/templates/.husky/commit-msg +1 -0
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -7,23 +7,20 @@
|
|
|
7
7
|
* PreToolUse Hook処理のオーケストレーション
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { AsyncHookToCliTranslator } from '../../domain/services/hook-to-cli-translator.js';
|
|
11
|
-
import { HookEvent } from '../../domain/value-objects/hook-event.js';
|
|
12
|
-
import type { BlockMetadata } from '../../domain/value-objects/hook-translation-result.js';
|
|
13
|
-
import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
|
|
14
|
-
import type { PhaseGateQueryPort } from '../../domain/ports/phase-gate-query-port.js';
|
|
15
|
-
import type { StoryReflectionQueryPort } from '../../domain/ports/story-reflection-query-port.js';
|
|
16
|
-
import type { FullModeRequirementQueryPort } from '../../domain/ports/full-mode-requirement-query-port.js';
|
|
17
10
|
import type {
|
|
18
11
|
BaselineGrandfatherCheckResult,
|
|
19
12
|
BaselineGrandfatherQueryPort,
|
|
20
|
-
} from
|
|
21
|
-
import type {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from
|
|
25
|
-
import {
|
|
26
|
-
import
|
|
13
|
+
} from "../../domain/ports/baseline-grandfather-query-port.js";
|
|
14
|
+
import type { ConfigQueryPort } from "../../domain/ports/config-query-port.js";
|
|
15
|
+
import type { ErrorGuidance, ErrorGuidanceQueryPort } from "../../domain/ports/error-guidance-query-port.js";
|
|
16
|
+
import type { FullModeRequirementQueryPort } from "../../domain/ports/full-mode-requirement-query-port.js";
|
|
17
|
+
import type { PhaseGateQueryPort } from "../../domain/ports/phase-gate-query-port.js";
|
|
18
|
+
import type { StoryReflectionQueryPort } from "../../domain/ports/story-reflection-query-port.js";
|
|
19
|
+
import { AsyncHookToCliTranslator } from "../../domain/services/hook-to-cli-translator.js";
|
|
20
|
+
import { HookEvent } from "../../domain/value-objects/hook-event.js";
|
|
21
|
+
import type { BlockMetadata } from "../../domain/value-objects/hook-translation-result.js";
|
|
22
|
+
import { WriteTargetScope } from "../../domain/value-objects/write-target-scope.js";
|
|
23
|
+
import type { HandlePreToolUseInput, HandlePreToolUseOutput } from "../dto/handle-pre-tool-use-dto.js";
|
|
27
24
|
|
|
28
25
|
export interface HandlePreToolUseUseCasePorts {
|
|
29
26
|
configQueryPort: ConfigQueryPort;
|
|
@@ -38,14 +35,17 @@ export interface HandlePreToolUseUseCasePorts {
|
|
|
38
35
|
export class HandlePreToolUseInputValidationError extends Error {
|
|
39
36
|
constructor(message: string) {
|
|
40
37
|
super(message);
|
|
41
|
-
this.name =
|
|
38
|
+
this.name = "HandlePreToolUseInputValidationError";
|
|
42
39
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
export class HandlePreToolUseUseCase {
|
|
47
44
|
private static readonly WRITE_TOOLS: ReadonlySet<string> = new Set([
|
|
48
|
-
|
|
45
|
+
"Write",
|
|
46
|
+
"Edit",
|
|
47
|
+
"NotebookEdit",
|
|
48
|
+
"str_replace_editor",
|
|
49
49
|
]);
|
|
50
50
|
|
|
51
51
|
private readonly translator: AsyncHookToCliTranslator;
|
|
@@ -55,10 +55,7 @@ export class HandlePreToolUseUseCase {
|
|
|
55
55
|
private readonly fullModeRequirementQueryPort?: FullModeRequirementQueryPort;
|
|
56
56
|
private readonly baselineGrandfatherQueryPort?: BaselineGrandfatherQueryPort;
|
|
57
57
|
private readonly errorGuidanceQueryPort?: ErrorGuidanceQueryPort;
|
|
58
|
-
private readonly grandfatherLogger: (
|
|
59
|
-
reason: string,
|
|
60
|
-
targetFilePaths: readonly string[],
|
|
61
|
-
) => void;
|
|
58
|
+
private readonly grandfatherLogger: (reason: string, targetFilePaths: readonly string[]) => void;
|
|
62
59
|
|
|
63
60
|
constructor(ports: HandlePreToolUseUseCasePorts) {
|
|
64
61
|
this.configQueryPort = ports.configQueryPort;
|
|
@@ -69,10 +66,7 @@ export class HandlePreToolUseUseCase {
|
|
|
69
66
|
this.errorGuidanceQueryPort = ports.errorGuidanceQueryPort;
|
|
70
67
|
this.grandfatherLogger =
|
|
71
68
|
ports.grandfatherLogger ??
|
|
72
|
-
((reason, paths) =>
|
|
73
|
-
process.stderr.write(
|
|
74
|
-
`[baseline] grandfather skip (${reason}): ${paths.join(', ')}\n`,
|
|
75
|
-
));
|
|
69
|
+
((reason, paths) => process.stderr.write(`[baseline] grandfather skip (${reason}): ${paths.join(", ")}\n`));
|
|
76
70
|
this.translator = new AsyncHookToCliTranslator({
|
|
77
71
|
configQueryPort: ports.configQueryPort,
|
|
78
72
|
reentryGuard: { isActive: () => false } as never,
|
|
@@ -83,7 +77,10 @@ export class HandlePreToolUseUseCase {
|
|
|
83
77
|
|
|
84
78
|
private async isFullModeBypassedByDesignDocs(targetFilePaths: readonly string[]): Promise<boolean> {
|
|
85
79
|
const unitId = this.deriveUnitIdFromPaths(targetFilePaths);
|
|
86
|
-
if (unitId === undefined || unitId ===
|
|
80
|
+
if (unitId === undefined || unitId === "") {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (typeof this.phaseGateQueryPort.checkDesignDocsExist !== "function") {
|
|
87
84
|
return false;
|
|
88
85
|
}
|
|
89
86
|
|
|
@@ -95,8 +92,8 @@ export class HandlePreToolUseUseCase {
|
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
async execute(input: HandlePreToolUseInput): Promise<HandlePreToolUseOutput> {
|
|
98
|
-
if (!input.toolName || input.toolName.trim() ===
|
|
99
|
-
throw new HandlePreToolUseInputValidationError(
|
|
95
|
+
if (!input.toolName || input.toolName.trim() === "") {
|
|
96
|
+
throw new HandlePreToolUseInputValidationError("toolNameは必須です(空文字不可)");
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
const grandfather = await this.checkGrandfather(input.targetFilePaths);
|
|
@@ -108,18 +105,17 @@ export class HandlePreToolUseUseCase {
|
|
|
108
105
|
const metadata = result.blockMetadata;
|
|
109
106
|
const blockedFilePath = metadata?.blockedFilePath ?? input.targetFilePaths[0];
|
|
110
107
|
|
|
111
|
-
if (metadata?.reason ===
|
|
108
|
+
if (metadata?.reason === "PROTECTED_FILE") {
|
|
112
109
|
return HandlePreToolUseUseCase.buildProtectedFileBlockOutput(blockedFilePath);
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
if (metadata?.reason ===
|
|
112
|
+
if (metadata?.reason === "PHASE_GATE") {
|
|
116
113
|
if (grandfather.allGrandfathered) {
|
|
117
|
-
this.grandfatherLogger(
|
|
114
|
+
this.grandfatherLogger("phase-gate", input.targetFilePaths);
|
|
118
115
|
// fallthrough: continue to full-mode / story-reflection checks (which may also grandfather)
|
|
119
116
|
} else {
|
|
120
|
-
const guidance = await this.resolveGuidance(
|
|
121
|
-
const unitIdForGuidance =
|
|
122
|
-
metadata?.unitId ?? this.deriveUnitIdFromPaths(input.targetFilePaths);
|
|
117
|
+
const guidance = await this.resolveGuidance("L2-001");
|
|
118
|
+
const unitIdForGuidance = metadata?.unitId ?? this.deriveUnitIdFromPaths(input.targetFilePaths);
|
|
123
119
|
return HandlePreToolUseUseCase.buildPhaseGateBlockOutput(
|
|
124
120
|
blockedFilePath,
|
|
125
121
|
metadata,
|
|
@@ -132,17 +128,19 @@ export class HandlePreToolUseUseCase {
|
|
|
132
128
|
shouldBlock: true,
|
|
133
129
|
blockedFilePath,
|
|
134
130
|
error: {
|
|
135
|
-
message: `ブロックされました: ${blockedFilePath ??
|
|
131
|
+
message: `ブロックされました: ${blockedFilePath ?? "不明なファイル"}`,
|
|
136
132
|
},
|
|
137
133
|
};
|
|
138
134
|
}
|
|
139
135
|
}
|
|
140
136
|
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
137
|
+
if (
|
|
138
|
+
HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName) &&
|
|
139
|
+
this.fullModeRequirementQueryPort !== undefined &&
|
|
140
|
+
input.targetFilePaths.length > 0
|
|
141
|
+
) {
|
|
144
142
|
if (grandfather.allGrandfathered) {
|
|
145
|
-
this.grandfatherLogger(
|
|
143
|
+
this.grandfatherLogger("full-mode", input.targetFilePaths);
|
|
146
144
|
} else {
|
|
147
145
|
const fullModeResult = await this.fullModeRequirementQueryPort.check(input.targetFilePaths);
|
|
148
146
|
if (fullModeResult.requiresFullMode) {
|
|
@@ -150,7 +148,7 @@ export class HandlePreToolUseUseCase {
|
|
|
150
148
|
//(hook がスキルコンテキストを参照できない構造的ギャップへの対処)
|
|
151
149
|
const bypassedByDesignDocs = await this.isFullModeBypassedByDesignDocs(input.targetFilePaths);
|
|
152
150
|
if (!bypassedByDesignDocs) {
|
|
153
|
-
const guidance = await this.resolveGuidance(
|
|
151
|
+
const guidance = await this.resolveGuidance("L2-001");
|
|
154
152
|
const unitIdForGuidance = this.deriveUnitIdFromPaths(input.targetFilePaths);
|
|
155
153
|
return HandlePreToolUseUseCase.buildFullModeRequiredBlockOutput(
|
|
156
154
|
input.targetFilePaths[0],
|
|
@@ -169,11 +167,16 @@ export class HandlePreToolUseUseCase {
|
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
if (grandfather.allGrandfathered) {
|
|
172
|
-
this.grandfatherLogger(
|
|
170
|
+
this.grandfatherLogger("story-reflection", input.targetFilePaths);
|
|
171
|
+
return { shouldBlock: false };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const unitId = scope.unitId;
|
|
175
|
+
if (unitId === undefined) {
|
|
173
176
|
return { shouldBlock: false };
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
const reflectionResult = await this.storyReflectionQueryPort.checkReflection(
|
|
179
|
+
const reflectionResult = await this.storyReflectionQueryPort.checkReflection(unitId);
|
|
177
180
|
|
|
178
181
|
if (reflectionResult.skipped || reflectionResult.passed) {
|
|
179
182
|
return { shouldBlock: false };
|
|
@@ -186,9 +189,7 @@ export class HandlePreToolUseUseCase {
|
|
|
186
189
|
);
|
|
187
190
|
}
|
|
188
191
|
|
|
189
|
-
private async checkGrandfather(
|
|
190
|
-
targetFilePaths: readonly string[],
|
|
191
|
-
): Promise<BaselineGrandfatherCheckResult> {
|
|
192
|
+
private async checkGrandfather(targetFilePaths: readonly string[]): Promise<BaselineGrandfatherCheckResult> {
|
|
192
193
|
if (this.baselineGrandfatherQueryPort === undefined) {
|
|
193
194
|
return {
|
|
194
195
|
allGrandfathered: false,
|
|
@@ -222,17 +223,15 @@ export class HandlePreToolUseUseCase {
|
|
|
222
223
|
blockedFilePath: string | undefined,
|
|
223
224
|
result: {
|
|
224
225
|
requiresFullMode: boolean;
|
|
225
|
-
rejectionRule?:
|
|
226
|
+
rejectionRule?: "MIXED_CHANGES" | "NEW_DOMAIN" | "API_CONTRACT";
|
|
226
227
|
rejectionReason?: string;
|
|
227
228
|
dominantCategory?: string;
|
|
228
229
|
},
|
|
229
230
|
guidance: ErrorGuidance | null,
|
|
230
231
|
unitId: string | undefined,
|
|
231
232
|
): HandlePreToolUseOutput {
|
|
232
|
-
const fp = blockedFilePath ??
|
|
233
|
-
const lines: string[] = [
|
|
234
|
-
`Full mode 必須変更が検出されました: ${fp}`,
|
|
235
|
-
];
|
|
233
|
+
const fp = blockedFilePath ?? "不明なファイル";
|
|
234
|
+
const lines: string[] = [`Full mode 必須変更が検出されました: ${fp}`];
|
|
236
235
|
if (result.dominantCategory) {
|
|
237
236
|
lines.push(`カテゴリ: ${result.dominantCategory}`);
|
|
238
237
|
}
|
|
@@ -242,31 +241,28 @@ export class HandlePreToolUseUseCase {
|
|
|
242
241
|
if (result.rejectionReason) {
|
|
243
242
|
lines.push(`理由: ${result.rejectionReason}`);
|
|
244
243
|
}
|
|
245
|
-
const suggestedSkill = guidance?.suggestedSkill ??
|
|
244
|
+
const suggestedSkill = guidance?.suggestedSkill ?? "/story-implementor";
|
|
246
245
|
lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
|
|
247
246
|
HandlePreToolUseUseCase.appendGuidanceLines(lines, guidance, unitId);
|
|
248
247
|
|
|
249
248
|
return {
|
|
250
249
|
shouldBlock: true,
|
|
251
250
|
blockedFilePath,
|
|
252
|
-
blockReason:
|
|
253
|
-
error: { message: lines.join(
|
|
251
|
+
blockReason: "FULL_MODE_REQUIRED",
|
|
252
|
+
error: { message: lines.join("\n") },
|
|
254
253
|
fullModeRejectionRule: result.rejectionRule,
|
|
255
254
|
fullModeDominantCategory: result.dominantCategory,
|
|
256
255
|
nextAction: suggestedSkill,
|
|
257
256
|
};
|
|
258
257
|
}
|
|
259
258
|
|
|
260
|
-
private static appendGuidanceLines(
|
|
261
|
-
lines: string[],
|
|
262
|
-
guidance: ErrorGuidance | null,
|
|
263
|
-
unitId?: string,
|
|
264
|
-
): void {
|
|
259
|
+
private static appendGuidanceLines(lines: string[], guidance: ErrorGuidance | null, unitId?: string): void {
|
|
265
260
|
if (guidance === null) return;
|
|
266
261
|
if (guidance.scaffoldCommand !== null) {
|
|
267
|
-
const command =
|
|
268
|
-
|
|
269
|
-
|
|
262
|
+
const command =
|
|
263
|
+
unitId !== undefined && unitId !== ""
|
|
264
|
+
? guidance.scaffoldCommand.replaceAll("<unit-id>", unitId)
|
|
265
|
+
: guidance.scaffoldCommand;
|
|
270
266
|
lines.push(` scaffold: ${command}`);
|
|
271
267
|
}
|
|
272
268
|
if (guidance.templatePath !== null) {
|
|
@@ -291,8 +287,17 @@ export class HandlePreToolUseUseCase {
|
|
|
291
287
|
}
|
|
292
288
|
|
|
293
289
|
const projectPaths = this.configQueryPort.getProjectPaths();
|
|
290
|
+
const inceptionPath = projectPaths.getDocsInception();
|
|
294
291
|
|
|
295
292
|
for (const targetFilePath of input.targetFilePaths) {
|
|
293
|
+
// WI-026 G1: inception 配下の書込は Phase 1 work であり Phase 3 reflection 対象外。
|
|
294
|
+
// 仕様上 Phase 3 = scripts/harness/{unit}/(domain|application|infrastructure|presentation)/*.ts
|
|
295
|
+
// のみが reflection check の対象。inception 編集を含めると _cross/{WI-XXX}/ 編集が
|
|
296
|
+
// 仮想パス docs/product/construction/_cross/ への反映を要求し常時 block される。
|
|
297
|
+
if (HandlePreToolUseUseCase.isUnderInception(targetFilePath, inceptionPath)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
296
301
|
const scope = WriteTargetScope.fromPath(targetFilePath, projectPaths);
|
|
297
302
|
if (scope?.level === 3 && scope.unitId !== undefined) {
|
|
298
303
|
return scope;
|
|
@@ -302,10 +307,16 @@ export class HandlePreToolUseUseCase {
|
|
|
302
307
|
return null;
|
|
303
308
|
}
|
|
304
309
|
|
|
310
|
+
private static isUnderInception(targetFilePath: string, inceptionPath: string): boolean {
|
|
311
|
+
const normalizedTarget = targetFilePath.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
|
|
312
|
+
const normalizedBase = inceptionPath.replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/").replace(/\/$/, "");
|
|
313
|
+
return normalizedTarget === normalizedBase || normalizedTarget.startsWith(`${normalizedBase}/`);
|
|
314
|
+
}
|
|
315
|
+
|
|
305
316
|
private static readonly LEVEL_LABELS: Record<number, string> = {
|
|
306
|
-
1:
|
|
307
|
-
2:
|
|
308
|
-
3:
|
|
317
|
+
1: "プロダクト設計",
|
|
318
|
+
2: "構築設計",
|
|
319
|
+
3: "実装",
|
|
309
320
|
};
|
|
310
321
|
|
|
311
322
|
private static buildPhaseGateBlockOutput(
|
|
@@ -315,22 +326,22 @@ export class HandlePreToolUseUseCase {
|
|
|
315
326
|
unitId: string | undefined,
|
|
316
327
|
): HandlePreToolUseOutput {
|
|
317
328
|
const levelLabel = metadata.scopeLevel
|
|
318
|
-
? HandlePreToolUseUseCase.LEVEL_LABELS[metadata.scopeLevel] ?? `Level ${metadata.scopeLevel}`
|
|
319
|
-
:
|
|
329
|
+
? (HandlePreToolUseUseCase.LEVEL_LABELS[metadata.scopeLevel] ?? `Level ${metadata.scopeLevel}`)
|
|
330
|
+
: "不明";
|
|
320
331
|
const blockers = metadata.phaseGateBlockers ?? [];
|
|
321
332
|
const lines: string[] = [
|
|
322
|
-
`フェーズゲート違反: ${blockedFilePath ??
|
|
323
|
-
`対象スコープ: Level ${metadata.scopeLevel ??
|
|
333
|
+
`フェーズゲート違反: ${blockedFilePath ?? "不明なファイル"}`,
|
|
334
|
+
`対象スコープ: Level ${metadata.scopeLevel ?? "?"} (${levelLabel})${metadata.unitId ? `, Unit: ${metadata.unitId}` : ""}`,
|
|
324
335
|
];
|
|
325
336
|
|
|
326
337
|
if (blockers.length > 0) {
|
|
327
|
-
lines.push(
|
|
338
|
+
lines.push("ブロック理由:");
|
|
328
339
|
for (const b of blockers) {
|
|
329
340
|
lines.push(` - ${b}`);
|
|
330
341
|
}
|
|
331
342
|
}
|
|
332
343
|
|
|
333
|
-
const suggestedSkill = guidance?.suggestedSkill ??
|
|
344
|
+
const suggestedSkill = guidance?.suggestedSkill ?? "/story-implementor";
|
|
334
345
|
lines.push(`次のアクション: ${suggestedSkill} スキルを使用して設計フェーズから開始してください。`);
|
|
335
346
|
if (metadata.unitId) {
|
|
336
347
|
lines.push(` 実行例: ${suggestedSkill} --unit ${metadata.unitId}`);
|
|
@@ -340,12 +351,10 @@ export class HandlePreToolUseUseCase {
|
|
|
340
351
|
return {
|
|
341
352
|
shouldBlock: true,
|
|
342
353
|
blockedFilePath,
|
|
343
|
-
blockReason:
|
|
344
|
-
error: { message: lines.join(
|
|
354
|
+
blockReason: "PHASE_GATE",
|
|
355
|
+
error: { message: lines.join("\n") },
|
|
345
356
|
phaseGateBlockers: [...blockers],
|
|
346
|
-
nextAction: metadata.unitId
|
|
347
|
-
? `${suggestedSkill} --unit ${metadata.unitId}`
|
|
348
|
-
: suggestedSkill,
|
|
357
|
+
nextAction: metadata.unitId ? `${suggestedSkill} --unit ${metadata.unitId}` : suggestedSkill,
|
|
349
358
|
};
|
|
350
359
|
}
|
|
351
360
|
|
|
@@ -375,13 +384,9 @@ export class HandlePreToolUseUseCase {
|
|
|
375
384
|
},
|
|
376
385
|
];
|
|
377
386
|
|
|
378
|
-
private static buildProtectedFileBlockOutput(
|
|
379
|
-
blockedFilePath
|
|
380
|
-
|
|
381
|
-
const fp = blockedFilePath ?? '不明なファイル';
|
|
382
|
-
const matched = HandlePreToolUseUseCase.PROTECTED_FILE_GUIDANCE.find(
|
|
383
|
-
({ pattern }) => pattern.test(fp),
|
|
384
|
-
);
|
|
387
|
+
private static buildProtectedFileBlockOutput(blockedFilePath: string | undefined): HandlePreToolUseOutput {
|
|
388
|
+
const fp = blockedFilePath ?? "不明なファイル";
|
|
389
|
+
const matched = HandlePreToolUseUseCase.PROTECTED_FILE_GUIDANCE.find(({ pattern }) => pattern.test(fp));
|
|
385
390
|
const message = matched
|
|
386
391
|
? matched.message(fp)
|
|
387
392
|
: `保護ファイルへの書き込みがブロックされました: ${fp}\nこのファイルは保護されています。/quick-implementor スキルで変更可能か確認してください。`;
|
|
@@ -389,7 +394,7 @@ export class HandlePreToolUseUseCase {
|
|
|
389
394
|
return {
|
|
390
395
|
shouldBlock: true,
|
|
391
396
|
blockedFilePath,
|
|
392
|
-
blockReason:
|
|
397
|
+
blockReason: "PROTECTED_FILE",
|
|
393
398
|
error: { message },
|
|
394
399
|
};
|
|
395
400
|
}
|
|
@@ -402,7 +407,7 @@ export class HandlePreToolUseUseCase {
|
|
|
402
407
|
return {
|
|
403
408
|
shouldBlock: true,
|
|
404
409
|
blockedFilePath,
|
|
405
|
-
blockReason:
|
|
410
|
+
blockReason: "STORY_REFLECTION",
|
|
406
411
|
error: {
|
|
407
412
|
message: HandlePreToolUseUseCase.buildStoryReflectionErrorMessage(blockers),
|
|
408
413
|
},
|
|
@@ -413,39 +418,42 @@ export class HandlePreToolUseUseCase {
|
|
|
413
418
|
|
|
414
419
|
private static buildStoryReflectionErrorMessage(blockers: readonly string[]): string {
|
|
415
420
|
const firstBlocker = blockers[0];
|
|
416
|
-
const details =
|
|
417
|
-
? null
|
|
418
|
-
: HandlePreToolUseUseCase.extractStoryReflectionDetails(firstBlocker);
|
|
421
|
+
const details =
|
|
422
|
+
firstBlocker === undefined ? null : HandlePreToolUseUseCase.extractStoryReflectionDetails(firstBlocker);
|
|
419
423
|
const lines: string[] = [];
|
|
420
424
|
|
|
425
|
+
const annotationKey = HandlePreToolUseUseCase.annotationKeyFor(details?.storyId);
|
|
426
|
+
|
|
421
427
|
if (details !== null) {
|
|
422
428
|
lines.push(`[L2-STORY-REFLECTION] ${details.productPath} に`);
|
|
423
|
-
lines.push(
|
|
424
|
-
lines.push(
|
|
429
|
+
lines.push(`@${annotationKey} ${details.storyId} が反映されていません。`);
|
|
430
|
+
lines.push("");
|
|
425
431
|
} else {
|
|
426
|
-
lines.push(
|
|
427
|
-
lines.push(
|
|
432
|
+
lines.push(`[L2-STORY-REFLECTION] product 文書に @${annotationKey} が反映されていません。`);
|
|
433
|
+
lines.push("");
|
|
428
434
|
}
|
|
429
435
|
|
|
430
436
|
for (const blocker of blockers) {
|
|
431
437
|
lines.push(`- ${blocker}`);
|
|
432
438
|
}
|
|
433
439
|
|
|
434
|
-
lines.push(
|
|
435
|
-
lines.push(
|
|
436
|
-
lines.push(
|
|
440
|
+
lines.push("");
|
|
441
|
+
lines.push("修正方法:");
|
|
442
|
+
lines.push(" 1. cascade-updater を実行して product 文書を更新");
|
|
437
443
|
lines.push(
|
|
438
|
-
` 2. または手動で該当 product 文書に
|
|
444
|
+
` 2. または手動で該当 product 文書に @${annotationKey} ${details?.storyId ?? "<WORK-ITEM-ID>"} を追加`,
|
|
439
445
|
);
|
|
440
|
-
lines.push(
|
|
441
|
-
lines.push(
|
|
446
|
+
lines.push("");
|
|
447
|
+
lines.push("参照: ADR-XXX");
|
|
448
|
+
|
|
449
|
+
return lines.join("\n");
|
|
450
|
+
}
|
|
442
451
|
|
|
443
|
-
|
|
452
|
+
private static annotationKeyFor(storyId: string | undefined): "work-item-id" | "story-id" {
|
|
453
|
+
return storyId !== undefined && /^WI-\d+$/.test(storyId) ? "work-item-id" : "story-id";
|
|
444
454
|
}
|
|
445
455
|
|
|
446
|
-
private static extractStoryReflectionDetails(
|
|
447
|
-
blocker: string,
|
|
448
|
-
): { productPath: string; storyId: string } | null {
|
|
456
|
+
private static extractStoryReflectionDetails(blocker: string): { productPath: string; storyId: string } | null {
|
|
449
457
|
const productPathMatch = blocker.match(/docs\/product\/construction\/[^\s]+\.md/);
|
|
450
458
|
const storyIdMatch = blocker.match(/@story-id\s+([A-Z][\w-]*-\d+)|\b([A-Z][\w-]*-\d+)\b/);
|
|
451
459
|
const storyId = storyIdMatch?.[1] ?? storyIdMatch?.[2];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @unit agent-integration
|
|
2
2
|
// @layer domain
|
|
3
3
|
|
|
4
|
-
import type {
|
|
5
|
-
import type {
|
|
4
|
+
import type { PhaseGateQueryResult } from "../value-objects/phase-gate-query-result.js";
|
|
5
|
+
import type { WriteTargetScope } from "../value-objects/write-target-scope.js";
|
|
6
6
|
|
|
7
7
|
export interface PhaseGateQueryPort {
|
|
8
8
|
checkGate(scope: WriteTargetScope, targetFilePath?: string): Promise<PhaseGateQueryResult>;
|
|
@@ -11,5 +11,5 @@ export interface PhaseGateQueryPort {
|
|
|
11
11
|
* 指定Unitの必須設計文書(logical_design.md / domain_model.md)が揃っているかを確認する。
|
|
12
12
|
* full mode 判定の bypass 条件として用いる(ISSUE-021)。
|
|
13
13
|
*/
|
|
14
|
-
checkDesignDocsExist(unitId: string): Promise<boolean>;
|
|
14
|
+
checkDesignDocsExist?(unitId: string): Promise<boolean>;
|
|
15
15
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// @unit agent-integration
|
|
2
2
|
// @layer domain
|
|
3
3
|
|
|
4
|
-
import { posix as path } from
|
|
5
|
-
import { WriteTargetScopeInvariantError } from
|
|
6
|
-
import {
|
|
7
|
-
import { ProjectPaths } from
|
|
4
|
+
import { posix as path } from "node:path";
|
|
5
|
+
import { WriteTargetScopeInvariantError } from "../errors/write-target-scope-invariant-error.js";
|
|
6
|
+
import type { PhaseGateLevel } from "../types/phase-gate-level.js";
|
|
7
|
+
import type { ProjectPaths } from "./project-paths.js";
|
|
8
8
|
|
|
9
9
|
type WriteTargetScopeProps = {
|
|
10
10
|
level: PhaseGateLevel;
|
|
@@ -27,19 +27,19 @@ export class WriteTargetScope {
|
|
|
27
27
|
|
|
28
28
|
static create(props: WriteTargetScopeProps): WriteTargetScope {
|
|
29
29
|
if (props.level !== 1 && props.level !== 2 && props.level !== 3) {
|
|
30
|
-
throw new WriteTargetScopeInvariantError(
|
|
30
|
+
throw new WriteTargetScopeInvariantError("level は 1, 2, 3 のいずれかである必要があります(INV-6違反)");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
if (props.level === 1 && (props.unitId !== undefined || props.storyId !== undefined)) {
|
|
34
|
-
throw new WriteTargetScopeInvariantError(
|
|
34
|
+
throw new WriteTargetScopeInvariantError("level=1 の場合、unitId と storyId は指定できません(INV-7違反)");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (props.level === 2 && (props.unitId === undefined || props.storyId !== undefined)) {
|
|
38
|
-
throw new WriteTargetScopeInvariantError(
|
|
38
|
+
throw new WriteTargetScopeInvariantError("level=2 の場合、unitId は必須で storyId は指定できません(INV-8違反)");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (props.level === 3 && props.unitId === undefined) {
|
|
42
|
-
throw new WriteTargetScopeInvariantError(
|
|
42
|
+
throw new WriteTargetScopeInvariantError("level=3 の場合、unitId は必須です(INV-9違反)");
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
return new WriteTargetScope(props);
|
|
@@ -48,11 +48,11 @@ export class WriteTargetScope {
|
|
|
48
48
|
static fromPath(filePath: string, projectPaths: ProjectPaths): WriteTargetScope | null {
|
|
49
49
|
const normalizedPath = normalize(filePath);
|
|
50
50
|
|
|
51
|
-
if (normalizedPath ===
|
|
51
|
+
if (normalizedPath === "") {
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
if (normalizedPath.includes(
|
|
55
|
+
if (normalizedPath.includes("/__tests__/") || normalizedPath.startsWith("__tests__/")) {
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -75,19 +75,18 @@ export class WriteTargetScope {
|
|
|
75
75
|
|
|
76
76
|
const inceptionMatch = matchPrefix(normalizedPath, inceptionPath);
|
|
77
77
|
if (inceptionMatch !== null) {
|
|
78
|
-
const [unitId, secondSegment
|
|
78
|
+
const [unitId, secondSegment] = inceptionMatch;
|
|
79
79
|
|
|
80
|
-
// 横断的
|
|
81
|
-
if (unitId ===
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
// 横断的 WI: docs/inception/_cross/{WI-XXX}/ → Level 3
|
|
81
|
+
if (unitId === "_cross") {
|
|
82
|
+
if (secondSegment !== undefined && WORK_ITEM_ID_PATTERN.test(secondSegment)) {
|
|
83
|
+
return WriteTargetScope.create({ level: 3, unitId, storyId: secondSegment });
|
|
84
|
+
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
if (unitId !== undefined && secondSegment === 'issues' && thirdSegment !== undefined && WORK_ITEM_ID_PATTERN.test(thirdSegment)) {
|
|
87
|
-
return WriteTargetScope.create({ level: 3, unitId, storyId: thirdSegment });
|
|
86
|
+
return WriteTargetScope.create({ level: 1 });
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
// 既存 US パス: docs/inception/{unit}/{storyId}/ → Level 3
|
|
89
|
+
// Unit 所有 WI / 既存 US パス: docs/inception/{unit}/{storyId}/ → Level 3
|
|
91
90
|
if (unitId !== undefined && secondSegment !== undefined && WORK_ITEM_ID_PATTERN.test(secondSegment)) {
|
|
92
91
|
return WriteTargetScope.create({ level: 3, unitId, storyId: secondSegment });
|
|
93
92
|
}
|
|
@@ -110,12 +109,7 @@ export class WriteTargetScope {
|
|
|
110
109
|
|
|
111
110
|
const productRoot = normalize(path.dirname(normalize(projectPaths.getDocsConstruction())));
|
|
112
111
|
const productMatch = matchPrefix(normalizedPath, productRoot);
|
|
113
|
-
if (
|
|
114
|
-
productMatch !== null
|
|
115
|
-
&& productMatch.length === 1
|
|
116
|
-
&& productMatch[0] !== undefined
|
|
117
|
-
&& productMatch[0].includes('.')
|
|
118
|
-
) {
|
|
112
|
+
if (productMatch !== null && productMatch.length === 1 && productMatch[0]?.includes(".")) {
|
|
119
113
|
return WriteTargetScope.create({ level: 1 });
|
|
120
114
|
}
|
|
121
115
|
|
|
@@ -123,14 +117,16 @@ export class WriteTargetScope {
|
|
|
123
117
|
}
|
|
124
118
|
|
|
125
119
|
equals(other: WriteTargetScope): boolean {
|
|
126
|
-
return this.level === other.level
|
|
127
|
-
&& this.unitId === other.unitId
|
|
128
|
-
&& this.storyId === other.storyId;
|
|
120
|
+
return this.level === other.level && this.unitId === other.unitId && this.storyId === other.storyId;
|
|
129
121
|
}
|
|
130
122
|
}
|
|
131
123
|
|
|
132
124
|
const normalize = (value: string): string =>
|
|
133
|
-
value
|
|
125
|
+
value
|
|
126
|
+
.replaceAll("\\", "/")
|
|
127
|
+
.replace(/^\.\/+/, "")
|
|
128
|
+
.replace(/\/+/g, "/")
|
|
129
|
+
.replace(/\/$/, "");
|
|
134
130
|
|
|
135
131
|
const matchPrefix = (targetPath: string, basePath: string): string[] | null => {
|
|
136
132
|
const normalizedBase = normalize(basePath);
|
|
@@ -144,5 +140,5 @@ const matchPrefix = (targetPath: string, basePath: string): string[] | null => {
|
|
|
144
140
|
}
|
|
145
141
|
|
|
146
142
|
const remainder = targetPath.slice(normalizedBase.length + 1);
|
|
147
|
-
return remainder ===
|
|
143
|
+
return remainder === "" ? [] : remainder.split("/");
|
|
148
144
|
};
|
|
@@ -105,7 +105,7 @@ export class FileSystemStoryReflectionQueryAdapter
|
|
|
105
105
|
): StoryReflectionQueryResult {
|
|
106
106
|
const warnings = result.warnings.map(
|
|
107
107
|
(w) =>
|
|
108
|
-
`${w.storyId}: ${w.productPath} に
|
|
108
|
+
`${w.storyId}: ${w.productPath} に @${FileSystemStoryReflectionQueryAdapter.annotationKeyFor(w.storyId)} ${w.storyId} が未反映 (optional, inception: ${w.inceptionPath})`,
|
|
109
109
|
);
|
|
110
110
|
|
|
111
111
|
if (!result.isBlocked()) {
|
|
@@ -114,9 +114,13 @@ export class FileSystemStoryReflectionQueryAdapter
|
|
|
114
114
|
|
|
115
115
|
const blockers = result.violations.map(
|
|
116
116
|
(v) =>
|
|
117
|
-
`${v.productPath} に
|
|
117
|
+
`${v.productPath} に @${FileSystemStoryReflectionQueryAdapter.annotationKeyFor(v.storyId)} ${v.storyId} が反映されていません (inception: ${v.inceptionPath})`,
|
|
118
118
|
);
|
|
119
119
|
|
|
120
120
|
return StoryReflectionQueryResult.block(blockers, warnings);
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
private static annotationKeyFor(storyId: string): "work-item-id" | "story-id" {
|
|
124
|
+
return /^WI-\d+$/.test(storyId) ? "work-item-id" : "story-id";
|
|
125
|
+
}
|
|
122
126
|
}
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* @unit biome-ast-engine
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { ArchitectureSpec } from '../../domain/value-objects/architecture-spec.js';
|
|
6
7
|
import type { RuleDefinition } from '../../domain/value-objects/rule-definition.js';
|
|
7
8
|
import type { RuleName } from '../../domain/value-objects/rule-name.js';
|
|
8
9
|
|
|
9
10
|
export type ResolveEnabledRulesOutput = {
|
|
10
11
|
readonly enabledRules: readonly RuleDefinition[];
|
|
11
12
|
readonly skippedRules: readonly RuleName[];
|
|
13
|
+
readonly architectureSpec: ArchitectureSpec;
|
|
12
14
|
};
|
package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts
CHANGED
|
@@ -3,15 +3,18 @@
|
|
|
3
3
|
* @unit biome-ast-engine
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { ArchitectureSpec } from '../../domain/value-objects/architecture-spec.js';
|
|
6
7
|
import type { RuleDefinition } from '../../domain/value-objects/rule-definition.js';
|
|
7
8
|
import type { RuleName } from '../../domain/value-objects/rule-name.js';
|
|
8
9
|
import type { ResolveEnabledRulesOutput } from '../dto/resolve-enabled-rules-output.js';
|
|
9
10
|
|
|
10
11
|
export const toResolveEnabledRulesOutput = (
|
|
11
12
|
enabledRules: readonly RuleDefinition[],
|
|
12
|
-
skippedRules: readonly RuleName[]
|
|
13
|
+
skippedRules: readonly RuleName[],
|
|
14
|
+
architectureSpec: ArchitectureSpec
|
|
13
15
|
): Readonly<ResolveEnabledRulesOutput> =>
|
|
14
16
|
Object.freeze({
|
|
15
17
|
enabledRules: Object.freeze([...enabledRules]),
|
|
16
18
|
skippedRules: Object.freeze([...skippedRules]),
|
|
19
|
+
architectureSpec,
|
|
17
20
|
});
|
package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts
CHANGED
|
@@ -33,7 +33,10 @@ export class AnalyzeImportGraphUseCase {
|
|
|
33
33
|
input: AnalyzeImportGraphInput = {}
|
|
34
34
|
): Promise<Readonly<AnalyzeImportGraphOutput>> {
|
|
35
35
|
const files = await this.workspaceFilePort.listSourceFiles(input.targets);
|
|
36
|
-
const snapshots = await this.sourceModuleAnalyzerPort.analyzeMany(
|
|
36
|
+
const snapshots = await this.sourceModuleAnalyzerPort.analyzeMany(
|
|
37
|
+
files,
|
|
38
|
+
input.architecture
|
|
39
|
+
);
|
|
37
40
|
const importGraph = this.importGraphBuilder.build(snapshots);
|
|
38
41
|
|
|
39
42
|
return toAnalyzeImportGraphOutput(files, snapshots, importGraph);
|