phasegate 0.91.0 → 0.107.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 (68) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.ja.md +15 -7
  3. package/README.md +24 -4
  4. package/docs/ADR/ADR-015-architecture-preset.md +183 -0
  5. package/docs/guide/codex-integration.md +7 -2
  6. package/docs/guide/installation.md +10 -2
  7. package/docs/guide/preset-selection.md +170 -0
  8. package/docs/guide/quick-vs-full-mode.md +3 -3
  9. package/docs/guide/retrofit-adoption.md +19 -2
  10. package/docs/guide/skills-overview.md +1 -1
  11. package/package.json +7 -1
  12. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -100
  13. package/scripts/harness/agent-integration/domain/ports/phase-gate-query-port.ts +3 -3
  14. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +26 -30
  15. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +6 -2
  16. package/scripts/harness/biome-ast-engine/application/dto/analyze-import-graph-input.ts +3 -0
  17. package/scripts/harness/biome-ast-engine/application/dto/resolve-enabled-rules-output.ts +2 -0
  18. package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts +4 -1
  19. package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts +4 -1
  20. package/scripts/harness/biome-ast-engine/application/usecases/execute-lint-usecase.ts +2 -0
  21. package/scripts/harness/biome-ast-engine/application/usecases/resolve-enabled-rules-usecase.ts +31 -3
  22. package/scripts/harness/biome-ast-engine/composition-root.ts +10 -2
  23. package/scripts/harness/biome-ast-engine/domain/ports/rule-config-provider-port.ts +16 -0
  24. package/scripts/harness/biome-ast-engine/domain/ports/source-module-analyzer-port.ts +5 -1
  25. package/scripts/harness/biome-ast-engine/domain/services/lint-runner.ts +5 -1
  26. package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +38 -0
  27. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-boundary.ts +7 -8
  28. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-name.ts +15 -23
  29. package/scripts/harness/biome-ast-engine/domain/value-objects/source-module-snapshot.ts +11 -4
  30. package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-config-provider-adapter.ts +29 -3
  31. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +22 -15
  32. package/scripts/harness/biome-ast-engine/infrastructure/mappers/source-module-snapshot-mapper.ts +26 -17
  33. package/scripts/harness/config-foundation/application/dto/resolved-config-output.ts +1 -0
  34. package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts +34 -2
  35. package/scripts/harness/config-foundation/application/usecases/migrate-schema-use-case.ts +89 -0
  36. package/scripts/harness/config-foundation/composition-root.ts +8 -0
  37. package/scripts/harness/config-foundation/domain/harness-config.ts +6 -0
  38. package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +257 -0
  39. package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +66 -0
  40. package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -0
  41. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +546 -0
  42. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +18 -6
  43. package/scripts/harness/config-foundation/presentation/cli/migrate-schema-command-handler.ts +83 -0
  44. package/scripts/harness/integrations/pre-commit.ts +211 -52
  45. package/scripts/harness/main.ts +460 -340
  46. package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -4
  47. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +54 -17
  48. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +154 -36
  49. package/scripts/harness/setup/skill-deployer.ts +140 -102
  50. package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +24 -23
  51. package/scripts/harness/skill-quality/domain/value-objects/commit-message.ts +23 -7
  52. package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts +52 -0
  53. package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts +29 -0
  54. package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +83 -18
  55. package/scripts/harness/traceability-model/composition-root.ts +48 -30
  56. package/scripts/harness/traceability-model/domain/ports/design-document-port.ts +9 -15
  57. package/scripts/harness/traceability-model/domain/ports/work-item-migration-apply-port.ts +11 -0
  58. package/scripts/harness/traceability-model/domain/ports/work-item-migration-source-port.ts +9 -0
  59. package/scripts/harness/traceability-model/domain/services/work-item-migration-planner.ts +162 -0
  60. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +57 -0
  61. package/scripts/harness/traceability-model/domain/value-objects/work-item-migration-candidate.ts +47 -0
  62. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-apply-gateway.ts +110 -0
  63. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-source-gateway.ts +182 -0
  64. package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +29 -43
  65. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +136 -0
  66. package/scripts/harness/traceability-model/presentation/cli/migrate-work-items-command-handler.ts +186 -0
  67. package/skills/quick-implementor/SKILL.md +17 -1
  68. package/templates/.husky/commit-msg +1 -0
@@ -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 '../../domain/ports/baseline-grandfather-query-port.js';
21
- import type {
22
- ErrorGuidance,
23
- ErrorGuidanceQueryPort,
24
- } from '../../domain/ports/error-guidance-query-port.js';
25
- import { WriteTargetScope } from '../../domain/value-objects/write-target-scope.js';
26
- import type { HandlePreToolUseInput, HandlePreToolUseOutput } from '../dto/handle-pre-tool-use-dto.js';
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 = 'HandlePreToolUseInputValidationError';
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
- 'Write', 'Edit', 'NotebookEdit', 'str_replace_editor',
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('toolNameは必須です(空文字不可)');
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 === 'PROTECTED_FILE') {
108
+ if (metadata?.reason === "PROTECTED_FILE") {
112
109
  return HandlePreToolUseUseCase.buildProtectedFileBlockOutput(blockedFilePath);
113
110
  }
114
111
 
115
- if (metadata?.reason === 'PHASE_GATE') {
112
+ if (metadata?.reason === "PHASE_GATE") {
116
113
  if (grandfather.allGrandfathered) {
117
- this.grandfatherLogger('phase-gate', input.targetFilePaths);
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('L2-001');
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 (HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName)
142
- && this.fullModeRequirementQueryPort !== undefined
143
- && input.targetFilePaths.length > 0) {
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('full-mode', input.targetFilePaths);
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('L2-001');
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('story-reflection', input.targetFilePaths);
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(scope.unitId!);
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?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
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 ?? '/story-implementor';
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: 'FULL_MODE_REQUIRED',
253
- error: { message: lines.join('\n') },
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 = unitId !== undefined && unitId !== ''
268
- ? guidance.scaffoldCommand.replaceAll('<unit-id>', unitId)
269
- : guidance.scaffoldCommand;
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 ?? '?'} (${levelLabel})${metadata.unitId ? `, Unit: ${metadata.unitId}` : ''}`,
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 ?? '/story-implementor';
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: 'PHASE_GATE',
344
- error: { message: lines.join('\n') },
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: string | undefined,
380
- ): HandlePreToolUseOutput {
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: 'PROTECTED_FILE',
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: 'STORY_REFLECTION',
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 = firstBlocker === undefined
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(`@story-id ${details.storyId} が反映されていません。`);
424
- lines.push('');
429
+ lines.push(`@${annotationKey} ${details.storyId} が反映されていません。`);
430
+ lines.push("");
425
431
  } else {
426
- lines.push('[L2-STORY-REFLECTION] product 文書に @story-id が反映されていません。');
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(' 1. cascade-updater を実行して product 文書を更新');
440
+ lines.push("");
441
+ lines.push("修正方法:");
442
+ lines.push(" 1. cascade-updater を実行して product 文書を更新");
437
443
  lines.push(
438
- ` 2. または手動で該当 product 文書に @story-id ${details?.storyId ?? '<STORY-ID>'} を追加`,
444
+ ` 2. または手動で該当 product 文書に @${annotationKey} ${details?.storyId ?? "<WORK-ITEM-ID>"} を追加`,
439
445
  );
440
- lines.push('');
441
- lines.push('参照: ADR-XXX');
446
+ lines.push("");
447
+ lines.push("参照: ADR-XXX");
448
+
449
+ return lines.join("\n");
450
+ }
442
451
 
443
- return lines.join('\n');
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 { WriteTargetScope } from '../value-objects/write-target-scope.js';
5
- import type { PhaseGateQueryResult } from '../value-objects/phase-gate-query-result.js';
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 '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 { ProjectPaths } from './project-paths.js';
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('level は 1, 2, 3 のいずれかである必要があります(INV-6違反)');
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('level=1 の場合、unitId と storyId は指定できません(INV-7違反)');
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('level=2 の場合、unitId は必須で storyId は指定できません(INV-8違反)');
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('level=3 の場合、unitId は必須です(INV-9違反)');
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('/__tests__/') || normalizedPath.startsWith('__tests__/')) {
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, thirdSegment] = inceptionMatch;
78
+ const [unitId, secondSegment] = inceptionMatch;
79
79
 
80
- // 横断的 issue: docs/inception/issues/{ISSUE-XXX}/ → Level 1
81
- if (unitId === 'issues') {
82
- return WriteTargetScope.create({ level: 1 });
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
- // Unit固有 issue: docs/inception/{unit}/issues/{ISSUE-XXX}/ → Level 3
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.replaceAll('\\', '/').replace(/^\.\/+/, '').replace(/\/+/g, '/').replace(/\/$/, '');
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 === '' ? [] : remainder.split('/');
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} に @story-id ${w.storyId} が未反映 (optional, inception: ${w.inceptionPath})`,
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} に @story-id ${v.storyId} が反映されていません (inception: ${v.inceptionPath})`,
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,6 +3,9 @@
3
3
  * @unit biome-ast-engine
4
4
  */
5
5
 
6
+ import type { ArchitectureSpec } from '../../domain/value-objects/architecture-spec.js';
7
+
6
8
  export type AnalyzeImportGraphInput = {
7
9
  readonly targets?: readonly string[];
10
+ readonly architecture?: ArchitectureSpec;
8
11
  };
@@ -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
  };
@@ -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
  });
@@ -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(files);
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);