phasegate 0.315.0 → 0.335.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/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
- package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
- package/docs/guide/cli-reference.md +89 -7
- package/docs/guide/configuration.md +43 -2
- package/docs/guide/quick-vs-full-mode.md +36 -1
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +16 -8
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +190 -2
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +6 -3
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
- package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
- package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
- package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
- package/scripts/harness/ci-governance/composition-root.ts +55 -3
- package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
- package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
- package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
- package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
- package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
- package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
- package/scripts/harness/main.ts +102 -12
- package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
- package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +4 -1
- package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +46 -17
- package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
- package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
- package/scripts/harness/quick-mode/index.ts +7 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
- package/skills/product-architect/SKILL.md +19 -0
- package/skills/story-mapper/SKILL.md +11 -0
- package/skills/story-writer/SKILL.md +11 -0
- package/skills/unit-designer/SKILL.md +11 -0
- package/templates/product_overview.template.md +85 -0
- package/templates/product_overview_plan.template.md +55 -0
- package/templates/story_mapping_plan.template.md +61 -0
- package/templates/story_writer_plan.template.md +61 -0
- package/templates/unit_design_plan.template.md +63 -0
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 副作用なし・純粋関数相当。対応パターンは以下:
|
|
11
11
|
* - リダイレクト `>` / `>>` / `>|` (clobber)
|
|
12
|
-
* - heredoc (`<<EOF > path`)
|
|
12
|
+
* - heredoc (`<<EOF > path`) — ヒアドキュメント**本文**はシェル構文解析の対象外
|
|
13
|
+
* (本文中の `<...>` をリダイレクトと誤認しないため。WI-362)
|
|
13
14
|
* - `tee` / `tee -a`
|
|
14
15
|
* - `sed -i` / `sed -i ''` (BSD)
|
|
15
16
|
* - `cp` / `mv` (宛先のみ)
|
|
@@ -111,6 +112,191 @@ function tokenize(input: string): Token[] {
|
|
|
111
112
|
return tokens;
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/** ヒアドキュメント開始演算子で宣言された終端デリミタ */
|
|
116
|
+
type HeredocDelimiter = {
|
|
117
|
+
/** クォート・エスケープを解除した終端語 */
|
|
118
|
+
readonly word: string;
|
|
119
|
+
/** `<<-` 形式か (終端行の先頭タブを無視する) */
|
|
120
|
+
readonly allowIndent: boolean;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** デリミタ語の終端となる文字 (空白およびシェル演算子) */
|
|
124
|
+
const DELIMITER_TERMINATORS = new Set([' ', '\t', ';', '&', '|', '<', '>', '(', ')']);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 1 行から ヒアドキュメント開始演算子 (`<<WORD` / `<<-WORD` / `<<'WORD'`) を検出し、
|
|
128
|
+
* 宣言順に終端デリミタを返す。
|
|
129
|
+
*
|
|
130
|
+
* クォート状態を追跡し、クォート内の `<<` は演算子として扱わない
|
|
131
|
+
* (例: `echo "a << b" > out.txt` の `<<` はヒアドキュメントではない)。
|
|
132
|
+
* `$( ... )` コマンド置換の内側ではクォート文脈がリセットされるため、
|
|
133
|
+
* `git commit -m "$(cat <<'EOF'` のような入れ子も検出できる。
|
|
134
|
+
* ヒアストリング `<<<` は本文を持たないため対象外。
|
|
135
|
+
*/
|
|
136
|
+
function findHeredocDelimiters(line: string): HeredocDelimiter[] {
|
|
137
|
+
const found: HeredocDelimiter[] = [];
|
|
138
|
+
const substitutionStack: ('none' | 'single' | 'double')[] = [];
|
|
139
|
+
let quote: 'none' | 'single' | 'double' = 'none';
|
|
140
|
+
let i = 0;
|
|
141
|
+
|
|
142
|
+
while (i < line.length) {
|
|
143
|
+
const ch = line[i];
|
|
144
|
+
|
|
145
|
+
if (quote === 'single') {
|
|
146
|
+
if (ch === "'") quote = 'none';
|
|
147
|
+
i += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (ch === '\\') {
|
|
152
|
+
i += 2;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (ch === '$' && line[i + 1] === '(') {
|
|
157
|
+
substitutionStack.push(quote);
|
|
158
|
+
quote = 'none';
|
|
159
|
+
i += 2;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (quote === 'double') {
|
|
164
|
+
if (ch === '"') quote = 'none';
|
|
165
|
+
i += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (ch === "'") {
|
|
170
|
+
quote = 'single';
|
|
171
|
+
i += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (ch === '"') {
|
|
176
|
+
quote = 'double';
|
|
177
|
+
i += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (ch === ')' && substitutionStack.length > 0) {
|
|
182
|
+
quote = substitutionStack.pop() ?? 'none';
|
|
183
|
+
i += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (ch === '<' && line[i + 1] === '<') {
|
|
188
|
+
if (line[i + 2] === '<') {
|
|
189
|
+
// ヒアストリング `<<<` は本文行を持たない
|
|
190
|
+
i += 3;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
i += 2;
|
|
194
|
+
let allowIndent = false;
|
|
195
|
+
if (line[i] === '-') {
|
|
196
|
+
allowIndent = true;
|
|
197
|
+
i += 1;
|
|
198
|
+
}
|
|
199
|
+
while (i < line.length && (line[i] === ' ' || line[i] === '\t')) i += 1;
|
|
200
|
+
const word = readHeredocDelimiterWord(line, i);
|
|
201
|
+
i = word.nextIndex;
|
|
202
|
+
if (word.value.length > 0) {
|
|
203
|
+
found.push({ word: word.value, allowIndent });
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
i += 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** `<<` 直後のデリミタ語を読む。クォート/バックスラッシュを解除した値を返す。 */
|
|
215
|
+
function readHeredocDelimiterWord(line: string, start: number): { value: string; nextIndex: number } {
|
|
216
|
+
let value = '';
|
|
217
|
+
let i = start;
|
|
218
|
+
|
|
219
|
+
while (i < line.length) {
|
|
220
|
+
const ch = line[i];
|
|
221
|
+
|
|
222
|
+
if (ch === "'") {
|
|
223
|
+
i += 1;
|
|
224
|
+
while (i < line.length && line[i] !== "'") {
|
|
225
|
+
value += line[i];
|
|
226
|
+
i += 1;
|
|
227
|
+
}
|
|
228
|
+
i += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (ch === '"') {
|
|
233
|
+
i += 1;
|
|
234
|
+
while (i < line.length && line[i] !== '"') {
|
|
235
|
+
if (line[i] === '\\' && i + 1 < line.length) {
|
|
236
|
+
value += line[i + 1];
|
|
237
|
+
i += 2;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
value += line[i];
|
|
241
|
+
i += 1;
|
|
242
|
+
}
|
|
243
|
+
i += 1;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (ch === '\\' && i + 1 < line.length) {
|
|
248
|
+
value += line[i + 1];
|
|
249
|
+
i += 2;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (DELIMITER_TERMINATORS.has(ch)) break;
|
|
254
|
+
|
|
255
|
+
value += ch;
|
|
256
|
+
i += 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { value, nextIndex: i };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 終端行判定 (`<<-` は先頭タブを無視し、行末の空白・CR は許容する) */
|
|
263
|
+
function isHeredocTerminator(line: string, delimiter: HeredocDelimiter): boolean {
|
|
264
|
+
const candidate = delimiter.allowIndent ? line.replace(/^\t+/, '') : line;
|
|
265
|
+
return candidate.trimEnd() === delimiter.word;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* ヒアドキュメント本文行を除去する。
|
|
270
|
+
*
|
|
271
|
+
* 本文はシェル構文ではなくデータであり、`Co-Authored-By: Claude <noreply@example.com>`
|
|
272
|
+
* のような `<...>` をリダイレクトと誤解釈すると無関係なコマンドが誤ブロックされる (WI-362)。
|
|
273
|
+
* 除去対象は**本文行と終端行のみ**で、開始演算子のある行はそのまま残すため
|
|
274
|
+
* `cat <<EOF > path` の `> path` は従来どおり検出される (fail-closed 維持)。
|
|
275
|
+
*/
|
|
276
|
+
function stripHeredocBodies(command: string): string {
|
|
277
|
+
if (!command.includes('<<')) return command;
|
|
278
|
+
|
|
279
|
+
const lines = command.split('\n');
|
|
280
|
+
const kept: string[] = [];
|
|
281
|
+
const pending: HeredocDelimiter[] = [];
|
|
282
|
+
|
|
283
|
+
for (const line of lines) {
|
|
284
|
+
if (pending.length > 0) {
|
|
285
|
+
// 本文行 / 終端行はいずれも構文解析対象から外す
|
|
286
|
+
if (isHeredocTerminator(line, pending[0])) {
|
|
287
|
+
pending.shift();
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
kept.push(line);
|
|
292
|
+
for (const delimiter of findHeredocDelimiters(line)) {
|
|
293
|
+
pending.push(delimiter);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return kept.join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
114
300
|
const OPERATOR_SEPARATORS = new Set(['&&', '||', ';', '|']);
|
|
115
301
|
|
|
116
302
|
/** トークン列を複合・パイプ境界で分割 */
|
|
@@ -311,7 +497,9 @@ function collectPositionals(tokens: Token[]): Token[] {
|
|
|
311
497
|
|
|
312
498
|
/** コマンド文字列を token 化 → 演算子分割 → 各コマンドから書き込み先を抽出する内部関数。 */
|
|
313
499
|
function extractFromCommandString(command: string): string[] {
|
|
314
|
-
|
|
500
|
+
// ヒアドキュメント本文はデータなのでシェル構文解析の対象外にする (WI-362)。
|
|
501
|
+
// apply_patch 抽出は元の command 文字列を直接走査するため影響を受けない。
|
|
502
|
+
const tokens = tokenize(stripHeredocBodies(command));
|
|
315
503
|
const groups = splitByOperators(tokens);
|
|
316
504
|
const collected: string[] = [];
|
|
317
505
|
for (const group of groups) {
|
|
@@ -25,6 +25,11 @@ const DEFAULT_PATTERNS = [
|
|
|
25
25
|
// grandfather bypass を防ぐため、書き込み自体を保護対象とする。
|
|
26
26
|
'.phasegate/baseline.json',
|
|
27
27
|
'**/.phasegate/baseline.json',
|
|
28
|
+
// WI-363: .husky/ 配下は L0 runtime の実施点(pre-commit / commit-msg / pre-push)。
|
|
29
|
+
// WI-352 で config カテゴリに分類されるようになり Quick Mode の書き込み許可対象に
|
|
30
|
+
// 入ったため、防御機構そのものの書き換えを protected file として明示的に止める。
|
|
31
|
+
'.husky/**',
|
|
32
|
+
'**/.husky/**',
|
|
28
33
|
];
|
|
29
34
|
|
|
30
35
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @unit agent-integration
|
|
2
2
|
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-373
|
|
3
4
|
|
|
4
5
|
import type {
|
|
5
6
|
FullModeRequirementQueryPort,
|
|
@@ -7,6 +8,7 @@ import type {
|
|
|
7
8
|
FullModeTargetChange,
|
|
8
9
|
} from '../../domain/ports/full-mode-requirement-query-port.js';
|
|
9
10
|
import type { ClassifyChangeCategoryUseCase } from '../../../quick-mode/application/usecases/classify-change-category-usecase.js';
|
|
11
|
+
import { QuickModeConfigError } from '../../../quick-mode/domain/errors/quick-mode-config-error.js';
|
|
10
12
|
|
|
11
13
|
export interface QuickModeFullModeRequirementAdapterDeps {
|
|
12
14
|
classifyUseCaseFactory: () => ClassifyChangeCategoryUseCase;
|
|
@@ -45,7 +47,19 @@ export class QuickModeFullModeRequirementAdapter implements FullModeRequirementQ
|
|
|
45
47
|
rejectionReason: contract.rejectionReason,
|
|
46
48
|
dominantCategory: contract.dominantCategory ?? undefined,
|
|
47
49
|
};
|
|
48
|
-
} catch {
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// WI-373: quickMode 設定そのものが不正な場合は fail-closed。
|
|
52
|
+
// allowedCategories / categoryOverrides の enum 検証が入ったことで
|
|
53
|
+
// config の typo が例外になるため、ここで一律 fail-open のままだと
|
|
54
|
+
// 「設定を壊すと全書き込みが素通りする」という最悪の穴になる。
|
|
55
|
+
// config 不在・IO エラー等その他の例外は WI-333 の fail-open を維持する。
|
|
56
|
+
if (error instanceof QuickModeConfigError) {
|
|
57
|
+
return {
|
|
58
|
+
requiresFullMode: true,
|
|
59
|
+
rejectionRule: 'MIXED_CHANGES',
|
|
60
|
+
rejectionReason: `quickMode 設定が不正なため Quick Mode 判定を実行できません: ${error.message}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
49
63
|
return { requiresFullMode: false };
|
|
50
64
|
}
|
|
51
65
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @work-item-id WI-208
|
|
7
7
|
* @work-item-id WI-345
|
|
8
8
|
* @work-item-id WI-347
|
|
9
|
+
* @work-item-id WI-376
|
|
9
10
|
*
|
|
10
11
|
* PreToolUse Hook Adapter
|
|
11
12
|
* Claude Code の PreToolUse Hook エントリポイント
|
|
@@ -25,10 +26,14 @@ import { createQuickModeCompositionRoot } from '../../quick-mode/composition-roo
|
|
|
25
26
|
import * as path from 'node:path';
|
|
26
27
|
import * as fs from 'node:fs/promises';
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* WI-376 (ADR-039): 呼び出し元 skill 名を受け取るフィールド(caller_skill)は持たない。
|
|
31
|
+
* Claude Code の PreToolUse payload に skill 情報は無く、エージェントの自己申告値は
|
|
32
|
+
* authorization / guidance の入力にしない。未知キーが来ても単に無視される。
|
|
33
|
+
*/
|
|
28
34
|
interface PreToolUseHookInput {
|
|
29
35
|
cwd?: string;
|
|
30
36
|
tool_name?: string;
|
|
31
|
-
caller_skill?: string;
|
|
32
37
|
tool_input?: {
|
|
33
38
|
path?: string;
|
|
34
39
|
file_path?: string;
|
|
@@ -199,11 +204,9 @@ async function main(): Promise<void> {
|
|
|
199
204
|
fullModeSessionQueryPort,
|
|
200
205
|
});
|
|
201
206
|
|
|
202
|
-
const callerSkill = input.caller_skill ?? process.env.PHASEGATE_CALLER_SKILL;
|
|
203
207
|
const output = await useCase.execute({
|
|
204
208
|
toolName: effectiveToolName,
|
|
205
209
|
targetFilePaths: projectTargetFilePaths,
|
|
206
|
-
callerSkill,
|
|
207
210
|
targetChanges: projectTargetChanges,
|
|
208
211
|
});
|
|
209
212
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
export interface ScaffoldInceptionOutput {
|
|
6
|
+
readonly targetPath: string;
|
|
7
|
+
readonly templatePath: string;
|
|
8
|
+
readonly kind: string;
|
|
9
|
+
readonly dryRun: boolean;
|
|
10
|
+
readonly written: boolean;
|
|
11
|
+
readonly alreadyExists: boolean;
|
|
12
|
+
readonly overwritten: boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-367
|
|
4
|
+
|
|
5
|
+
import type { TemplateCatalogPort } from "../../domain/ports/template-catalog-port.js";
|
|
6
|
+
|
|
7
|
+
export interface ListTemplatesOutputEntry {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly fileName: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ListTemplatesOutput {
|
|
13
|
+
readonly directoryPath: string;
|
|
14
|
+
readonly templates: readonly ListTemplatesOutputEntry[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ListTemplatesUseCase {
|
|
18
|
+
constructor(private readonly catalog: TemplateCatalogPort) {}
|
|
19
|
+
|
|
20
|
+
async execute(): Promise<ListTemplatesOutput> {
|
|
21
|
+
const entries = await this.catalog.list();
|
|
22
|
+
return {
|
|
23
|
+
directoryPath: this.catalog.directoryPath(),
|
|
24
|
+
templates: entries.map((entry) => ({
|
|
25
|
+
name: entry.name.value,
|
|
26
|
+
fileName: entry.fileName,
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
import type { InceptionDocWriterPort } from "../../domain/ports/inception-doc-writer-port.js";
|
|
6
|
+
import type { InceptionTemplateRepositoryPort } from "../../domain/ports/inception-template-repository-port.js";
|
|
7
|
+
import { InceptionDocKind } from "../../domain/value-objects/inception-doc-kind.js";
|
|
8
|
+
import type { ScaffoldInceptionInput } from "../dto/scaffold-inception-input.js";
|
|
9
|
+
import type { ScaffoldInceptionOutput } from "../dto/scaffold-inception-output.js";
|
|
10
|
+
|
|
11
|
+
export class ScaffoldInceptionUseCase {
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly templates: InceptionTemplateRepositoryPort,
|
|
14
|
+
private readonly writer: InceptionDocWriterPort,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
async execute(input: ScaffoldInceptionInput): Promise<ScaffoldInceptionOutput> {
|
|
18
|
+
const kind = InceptionDocKind.create(input.kind);
|
|
19
|
+
const dryRun = input.dryRun === true;
|
|
20
|
+
const force = input.force === true;
|
|
21
|
+
|
|
22
|
+
const templatePath = this.templates.resolvePath(kind);
|
|
23
|
+
const targetPath = this.writer.resolvePath(kind);
|
|
24
|
+
const alreadyExists = await this.writer.exists(kind);
|
|
25
|
+
|
|
26
|
+
if (dryRun) {
|
|
27
|
+
return {
|
|
28
|
+
targetPath,
|
|
29
|
+
templatePath,
|
|
30
|
+
kind: kind.value,
|
|
31
|
+
dryRun: true,
|
|
32
|
+
written: false,
|
|
33
|
+
alreadyExists,
|
|
34
|
+
overwritten: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (alreadyExists && !force) {
|
|
39
|
+
return {
|
|
40
|
+
targetPath,
|
|
41
|
+
templatePath,
|
|
42
|
+
kind: kind.value,
|
|
43
|
+
dryRun: false,
|
|
44
|
+
written: false,
|
|
45
|
+
alreadyExists: true,
|
|
46
|
+
overwritten: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const raw = await this.templates.read(kind);
|
|
51
|
+
await this.writer.write(kind, raw);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
targetPath,
|
|
55
|
+
templatePath,
|
|
56
|
+
kind: kind.value,
|
|
57
|
+
dryRun: false,
|
|
58
|
+
written: true,
|
|
59
|
+
alreadyExists,
|
|
60
|
+
overwritten: alreadyExists && force,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-367
|
|
4
|
+
|
|
5
|
+
import type { TemplateCatalogPort } from "../../domain/ports/template-catalog-port.js";
|
|
6
|
+
import { TemplateName } from "../../domain/value-objects/template-name.js";
|
|
7
|
+
|
|
8
|
+
export type ShowTemplateOutput =
|
|
9
|
+
| { readonly found: true; readonly name: string; readonly content: string }
|
|
10
|
+
| {
|
|
11
|
+
readonly found: false;
|
|
12
|
+
readonly reason: "invalid-name" | "not-found";
|
|
13
|
+
readonly availableNames: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class ShowTemplateUseCase {
|
|
17
|
+
constructor(private readonly catalog: TemplateCatalogPort) {}
|
|
18
|
+
|
|
19
|
+
async execute(rawName: string): Promise<ShowTemplateOutput> {
|
|
20
|
+
if (!TemplateName.isValid(rawName)) {
|
|
21
|
+
// 不正名では catalog を引かない。パス由来の情報を一切露出させない。
|
|
22
|
+
return { found: false, reason: "invalid-name", availableNames: [] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const name = TemplateName.create(rawName);
|
|
26
|
+
const content = await this.catalog.read(name);
|
|
27
|
+
|
|
28
|
+
if (content === null) {
|
|
29
|
+
const entries = await this.catalog.list();
|
|
30
|
+
return {
|
|
31
|
+
found: false,
|
|
32
|
+
reason: "not-found",
|
|
33
|
+
availableNames: entries.map((entry) => entry.name.value),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { found: true, name: name.value, content };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit ci-governance
|
|
4
|
+
* @work-item-id WI-367, WI-368, WI-369
|
|
4
5
|
*
|
|
5
6
|
* Composition Root - 依存性の組み立て
|
|
6
7
|
*/
|
|
@@ -10,6 +11,7 @@ import { CheckAgentContextUseCase } from "./application/usecases/check-agent-con
|
|
|
10
11
|
import { CheckEscalationUseCase } from "./application/usecases/check-escalation-usecase.js";
|
|
11
12
|
import { CreateBaselineUseCase } from "./application/usecases/create-baseline-usecase.js";
|
|
12
13
|
import { GenerateCiTemplateUseCase } from "./application/usecases/generate-ci-template-usecase.js";
|
|
14
|
+
import { ListTemplatesUseCase } from "./application/usecases/list-templates-usecase.js";
|
|
13
15
|
import { MigrateAgentsMdUseCase } from "./application/usecases/migrate-agents-md-usecase.js";
|
|
14
16
|
import { PinIntegrityUseCase } from "./application/usecases/pin-integrity-usecase.js";
|
|
15
17
|
import { RecordErrorOccurrenceUseCase } from "./application/usecases/record-error-occurrence-usecase.js";
|
|
@@ -18,6 +20,8 @@ import { RefreshClaudeMdUseCase } from "./application/usecases/refresh-claude-md
|
|
|
18
20
|
import { RenderCiTemplateUseCase } from "./application/usecases/render-ci-template-usecase.js";
|
|
19
21
|
import { ResetRepetitionUseCase } from "./application/usecases/reset-repetition-usecase.js";
|
|
20
22
|
import { ScaffoldDesignUseCase } from "./application/usecases/scaffold-design-usecase.js";
|
|
23
|
+
import { ScaffoldInceptionUseCase } from "./application/usecases/scaffold-inception-usecase.js";
|
|
24
|
+
import { ShowTemplateUseCase } from "./application/usecases/show-template-usecase.js";
|
|
21
25
|
import { ValidatePointersUseCase } from "./application/usecases/validate-pointers-usecase.js";
|
|
22
26
|
import { VerifyIntegrityUseCase } from "./application/usecases/verify-integrity-usecase.js";
|
|
23
27
|
import { ClaudeMdComposer } from "./domain/services/claude-md-composer.js";
|
|
@@ -34,8 +38,11 @@ import { ErrorRepetitionJsonRepository } from "./infrastructure/adapters/error-r
|
|
|
34
38
|
import { EscalationLogExecutorAdapter } from "./infrastructure/adapters/escalation-log-executor-adapter.js";
|
|
35
39
|
import { FileSystemDesignDocWriterAdapter } from "./infrastructure/adapters/file-system-design-doc-writer-adapter.js";
|
|
36
40
|
import { FileSystemExistenceAdapter } from "./infrastructure/adapters/file-system-existence-adapter.js";
|
|
41
|
+
import { FileSystemInceptionDocWriterAdapter } from "./infrastructure/adapters/file-system-inception-doc-writer-adapter.js";
|
|
42
|
+
import { FileSystemInceptionTemplateRepositoryAdapter } from "./infrastructure/adapters/file-system-inception-template-repository-adapter.js";
|
|
37
43
|
import { FileSystemSha1HasherAdapter } from "./infrastructure/adapters/file-system-sha1-hasher-adapter.js";
|
|
38
44
|
import { FileSystemSha256HasherAdapter } from "./infrastructure/adapters/file-system-sha256-hasher-adapter.js";
|
|
45
|
+
import { FileSystemTemplateCatalogAdapter } from "./infrastructure/adapters/file-system-template-catalog-adapter.js";
|
|
39
46
|
import { FileSystemTemplateRepositoryAdapter } from "./infrastructure/adapters/file-system-template-repository-adapter.js";
|
|
40
47
|
import { GlobFileScannerAdapter } from "./infrastructure/adapters/glob-file-scanner-adapter.js";
|
|
41
48
|
import { HarnessApiCommandExistenceAdapter } from "./infrastructure/adapters/harness-api-command-existence-adapter.js";
|
|
@@ -53,6 +60,8 @@ import { MigrateAgentsMdHandler } from "./presentation/handlers/migrate-agents-m
|
|
|
53
60
|
import { RefreshAgentContextHandler } from "./presentation/handlers/refresh-agent-context-handler.js";
|
|
54
61
|
import { RefreshClaudeMdHandler } from "./presentation/handlers/refresh-claude-md-handler.js";
|
|
55
62
|
import { ScaffoldDesignHandler } from "./presentation/handlers/scaffold-design-handler.js";
|
|
63
|
+
import { ScaffoldInceptionHandler } from "./presentation/handlers/scaffold-inception-handler.js";
|
|
64
|
+
import { TemplatesHandler } from "./presentation/handlers/templates-handler.js";
|
|
56
65
|
|
|
57
66
|
export interface CiGovernanceCompositionRoot {
|
|
58
67
|
generateCiTemplateHandler: GenerateCiTemplateHandler;
|
|
@@ -63,6 +72,8 @@ export interface CiGovernanceCompositionRoot {
|
|
|
63
72
|
checkRepetitionHandler: CheckRepetitionHandler;
|
|
64
73
|
createBaselineHandler: CreateBaselineHandler;
|
|
65
74
|
scaffoldDesignHandler: ScaffoldDesignHandler;
|
|
75
|
+
scaffoldInceptionHandler: ScaffoldInceptionHandler;
|
|
76
|
+
templatesHandler: TemplatesHandler;
|
|
66
77
|
// Use cases exposed for direct access
|
|
67
78
|
recordErrorOccurrenceUseCase: RecordErrorOccurrenceUseCase;
|
|
68
79
|
checkEscalationUseCase: CheckEscalationUseCase;
|
|
@@ -71,10 +82,33 @@ export interface CiGovernanceCompositionRoot {
|
|
|
71
82
|
validatePointersUseCase: ValidatePointersUseCase;
|
|
72
83
|
createBaselineUseCase: CreateBaselineUseCase;
|
|
73
84
|
scaffoldDesignUseCase: ScaffoldDesignUseCase;
|
|
85
|
+
scaffoldInceptionUseCase: ScaffoldInceptionUseCase;
|
|
74
86
|
integrityHandler: IntegrityHandler;
|
|
75
87
|
}
|
|
76
88
|
|
|
77
|
-
|
|
89
|
+
/**
|
|
90
|
+
* 解決済みの設計文書ルート(`phasegate.config.json` の `paths`)。
|
|
91
|
+
* 省略時は従来どおり `docs/product/construction` / `docs/inception` を使う。
|
|
92
|
+
*
|
|
93
|
+
* WI-369: これを渡さないと `scaffold-design` の書き込み先が
|
|
94
|
+
* `docs/product/construction` に固定され、`paths.designDocs` を移設した PJ で
|
|
95
|
+
* scaffold 先とフェーズゲートの検査先がズレる。
|
|
96
|
+
*/
|
|
97
|
+
export interface CiGovernanceDocPaths {
|
|
98
|
+
readonly designDocs?: string;
|
|
99
|
+
readonly inceptionDocs?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const DEFAULT_DESIGN_DOCS_ROOT = "docs/product/construction";
|
|
103
|
+
const DEFAULT_INCEPTION_DOCS_ROOT = "docs/inception";
|
|
104
|
+
|
|
105
|
+
export function buildCiGovernance(
|
|
106
|
+
baseDir: string,
|
|
107
|
+
harnessRoot: string = baseDir,
|
|
108
|
+
docPaths: CiGovernanceDocPaths = {},
|
|
109
|
+
): CiGovernanceCompositionRoot {
|
|
110
|
+
const designDocsRoot = docPaths.designDocs ?? DEFAULT_DESIGN_DOCS_ROOT;
|
|
111
|
+
const inceptionDocsRoot = docPaths.inceptionDocs ?? DEFAULT_INCEPTION_DOCS_ROOT;
|
|
78
112
|
// Infrastructure adapters
|
|
79
113
|
const validatorIdRegistryAdapter = new ValidatorIdRegistryAdapter();
|
|
80
114
|
const presetConfigAdapter = new PresetConfigAdapter();
|
|
@@ -119,11 +153,24 @@ export function buildCiGovernance(baseDir: string, harnessRoot: string = baseDir
|
|
|
119
153
|
const baselineRepository = new BaselineJsonRepositoryAdapter(baseDir);
|
|
120
154
|
const createBaselineUseCase = new CreateBaselineUseCase(fileScanner, fileHasher, baselineRepository);
|
|
121
155
|
|
|
122
|
-
// Scaffold design adapters & use case (ISSUE-007 Wave 4)
|
|
156
|
+
// Scaffold design adapters & use case (ISSUE-007 Wave 4 / WI-369 paths 追従)
|
|
123
157
|
const templateRepository = new FileSystemTemplateRepositoryAdapter(harnessRoot);
|
|
124
|
-
const designDocWriter = new FileSystemDesignDocWriterAdapter(baseDir);
|
|
158
|
+
const designDocWriter = new FileSystemDesignDocWriterAdapter(baseDir, designDocsRoot);
|
|
125
159
|
const scaffoldDesignUseCase = new ScaffoldDesignUseCase(templateRepository, designDocWriter);
|
|
126
160
|
|
|
161
|
+
// Scaffold inception adapters & use case (WI-368)
|
|
162
|
+
const inceptionTemplateRepository = new FileSystemInceptionTemplateRepositoryAdapter(harnessRoot);
|
|
163
|
+
const inceptionDocWriter = new FileSystemInceptionDocWriterAdapter(baseDir, {
|
|
164
|
+
designDocsRoot,
|
|
165
|
+
inceptionDocsRoot,
|
|
166
|
+
});
|
|
167
|
+
const scaffoldInceptionUseCase = new ScaffoldInceptionUseCase(inceptionTemplateRepository, inceptionDocWriter);
|
|
168
|
+
|
|
169
|
+
// Template catalog adapters & use cases (WI-367)
|
|
170
|
+
const templateCatalog = new FileSystemTemplateCatalogAdapter(harnessRoot);
|
|
171
|
+
const listTemplatesUseCase = new ListTemplatesUseCase(templateCatalog);
|
|
172
|
+
const showTemplateUseCase = new ShowTemplateUseCase(templateCatalog);
|
|
173
|
+
|
|
127
174
|
// Handlers
|
|
128
175
|
const generateCiTemplateHandler = new GenerateCiTemplateHandler(generateCiTemplateUseCase, renderCiTemplateUseCase);
|
|
129
176
|
const migrateAgentsMdHandler = new MigrateAgentsMdHandler(migrateAgentsMdUseCase, validatePointersUseCase);
|
|
@@ -133,6 +180,8 @@ export function buildCiGovernance(baseDir: string, harnessRoot: string = baseDir
|
|
|
133
180
|
const checkRepetitionHandler = new CheckRepetitionHandler(checkEscalationUseCase, resetRepetitionUseCase);
|
|
134
181
|
const createBaselineHandler = new CreateBaselineHandler(createBaselineUseCase);
|
|
135
182
|
const scaffoldDesignHandler = new ScaffoldDesignHandler(scaffoldDesignUseCase);
|
|
183
|
+
const scaffoldInceptionHandler = new ScaffoldInceptionHandler(scaffoldInceptionUseCase);
|
|
184
|
+
const templatesHandler = new TemplatesHandler(listTemplatesUseCase, showTemplateUseCase);
|
|
136
185
|
|
|
137
186
|
// Integrity pin adapters & handler (WI-254 / ADR-030 §Decision.3.①)
|
|
138
187
|
const integrityChecker = new IntegrityChecker();
|
|
@@ -156,6 +205,8 @@ export function buildCiGovernance(baseDir: string, harnessRoot: string = baseDir
|
|
|
156
205
|
checkRepetitionHandler,
|
|
157
206
|
createBaselineHandler,
|
|
158
207
|
scaffoldDesignHandler,
|
|
208
|
+
scaffoldInceptionHandler,
|
|
209
|
+
templatesHandler,
|
|
159
210
|
recordErrorOccurrenceUseCase,
|
|
160
211
|
checkEscalationUseCase,
|
|
161
212
|
resetRepetitionUseCase,
|
|
@@ -163,6 +214,7 @@ export function buildCiGovernance(baseDir: string, harnessRoot: string = baseDir
|
|
|
163
214
|
validatePointersUseCase,
|
|
164
215
|
createBaselineUseCase,
|
|
165
216
|
scaffoldDesignUseCase,
|
|
217
|
+
scaffoldInceptionUseCase,
|
|
166
218
|
integrityHandler,
|
|
167
219
|
};
|
|
168
220
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
import type { InceptionDocKind } from "../value-objects/inception-doc-kind.js";
|
|
6
|
+
|
|
7
|
+
export interface InceptionDocWriterPort {
|
|
8
|
+
/** 書き込み先の絶対パスを返す(副作用なし) */
|
|
9
|
+
resolvePath(kind: InceptionDocKind): string;
|
|
10
|
+
|
|
11
|
+
/** 書き込み先が既に存在するか */
|
|
12
|
+
exists(kind: InceptionDocKind): Promise<boolean>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* inception / product 文書を書き込む(親ディレクトリが無ければ作成)。
|
|
16
|
+
* 書き込んだ絶対パスを返す。
|
|
17
|
+
*/
|
|
18
|
+
write(kind: InceptionDocKind, content: string): Promise<string>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-368
|
|
4
|
+
|
|
5
|
+
import type { InceptionDocKind } from "../value-objects/inception-doc-kind.js";
|
|
6
|
+
|
|
7
|
+
export interface InceptionTemplateRepositoryPort {
|
|
8
|
+
/** テンプレートの絶対パスを返す(UI 出力用) */
|
|
9
|
+
resolvePath(kind: InceptionDocKind): string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* kind に対応するテンプレートファイルを読み込む。
|
|
13
|
+
* 存在しない場合は例外を投げる。
|
|
14
|
+
*/
|
|
15
|
+
read(kind: InceptionDocKind): Promise<string>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @unit ci-governance
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-367
|
|
4
|
+
|
|
5
|
+
import type { TemplateCatalogEntry } from "../value-objects/template-catalog-entry.js";
|
|
6
|
+
import type { TemplateName } from "../value-objects/template-name.js";
|
|
7
|
+
|
|
8
|
+
export interface TemplateCatalogPort {
|
|
9
|
+
/** テンプレートディレクトリの絶対パス(案内表示用) */
|
|
10
|
+
directoryPath(): string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 実在するテンプレートを name 昇順で列挙する。
|
|
14
|
+
* ディレクトリが存在しない場合は空配列を返す(例外にしない)。
|
|
15
|
+
*/
|
|
16
|
+
list(): Promise<readonly TemplateCatalogEntry[]>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* catalog に name 完全一致するテンプレート本文を読む。
|
|
20
|
+
* 一致するエントリが無い場合は null を返す。
|
|
21
|
+
*/
|
|
22
|
+
read(name: TemplateName): Promise<string | null>;
|
|
23
|
+
}
|