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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @work-item-id WI-372
|
|
5
|
+
*
|
|
6
|
+
* `quickMode.categoryOverrides` を表す値オブジェクト。
|
|
7
|
+
* ChangeCategory 7 値をキー、glob パターン列を値とする写像で、
|
|
8
|
+
* プロジェクト固有パス(`results/**` 等)を任意カテゴリへ割り当てる。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { QuickModeConfigError } from '../errors/quick-mode-config-error.js';
|
|
12
|
+
import {
|
|
13
|
+
CHANGE_CATEGORY_VALUES,
|
|
14
|
+
ChangeCategory,
|
|
15
|
+
type ChangeCategoryValue,
|
|
16
|
+
isChangeCategoryValue,
|
|
17
|
+
} from './change-category.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* glob → 正規表現の変換。
|
|
21
|
+
*
|
|
22
|
+
* domain 層は repo 全体で外部 npm パッケージを import していないため
|
|
23
|
+
* (picomatch は infrastructure adapter 専用)、
|
|
24
|
+
* `agent-integration/domain/value-objects/protected-file-list.ts` と同型の
|
|
25
|
+
* 純正規表現実装を採る(logical_design.md LD-10)。
|
|
26
|
+
*
|
|
27
|
+
* サポートする記法:
|
|
28
|
+
* - `**` … `/` を含む任意の文字列
|
|
29
|
+
* - `*` … `/` を含まない任意の文字列
|
|
30
|
+
* - `?` … `/` 以外の 1 文字
|
|
31
|
+
* - その他はリテラル(正規表現メタ文字はエスケープ)
|
|
32
|
+
*/
|
|
33
|
+
function globToRegExp(pattern: string): RegExp {
|
|
34
|
+
const regexSource = pattern
|
|
35
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
36
|
+
.replace(/\*\*/g, '__DOUBLE_STAR__')
|
|
37
|
+
.replace(/\*/g, '[^/]*')
|
|
38
|
+
.replace(/__DOUBLE_STAR__/g, '.*')
|
|
39
|
+
.replace(/\?/g, '[^/]');
|
|
40
|
+
return new RegExp(`^${regexSource}$`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type CategoryOverrideRulesInput = Readonly<Record<string, readonly string[]>>;
|
|
44
|
+
|
|
45
|
+
export class CategoryOverrideRules {
|
|
46
|
+
private readonly patterns: ReadonlyMap<ChangeCategoryValue, readonly string[]>;
|
|
47
|
+
private readonly matchers: ReadonlyMap<ChangeCategoryValue, readonly RegExp[]>;
|
|
48
|
+
|
|
49
|
+
private constructor(
|
|
50
|
+
patterns: ReadonlyMap<ChangeCategoryValue, readonly string[]>,
|
|
51
|
+
matchers: ReadonlyMap<ChangeCategoryValue, readonly RegExp[]>,
|
|
52
|
+
) {
|
|
53
|
+
this.patterns = patterns;
|
|
54
|
+
this.matchers = matchers;
|
|
55
|
+
Object.freeze(this);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static empty(): CategoryOverrideRules {
|
|
59
|
+
return new CategoryOverrideRules(new Map(), new Map());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static create(raw: CategoryOverrideRulesInput | undefined | null): CategoryOverrideRules {
|
|
63
|
+
if (raw === undefined || raw === null) {
|
|
64
|
+
return CategoryOverrideRules.empty();
|
|
65
|
+
}
|
|
66
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
67
|
+
throw new QuickModeConfigError('categoryOverrides must be an object keyed by change category');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const patterns = new Map<ChangeCategoryValue, readonly string[]>();
|
|
71
|
+
const matchers = new Map<ChangeCategoryValue, readonly RegExp[]>();
|
|
72
|
+
|
|
73
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
74
|
+
if (!isChangeCategoryValue(key)) {
|
|
75
|
+
throw new QuickModeConfigError(
|
|
76
|
+
`categoryOverrides contains unknown category "${key}". Valid values: ${CHANGE_CATEGORY_VALUES.join(', ')}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(value)) {
|
|
80
|
+
throw new QuickModeConfigError(`categoryOverrides.${key} must be an array of glob patterns`);
|
|
81
|
+
}
|
|
82
|
+
for (const pattern of value) {
|
|
83
|
+
if (typeof pattern !== 'string' || pattern.length === 0) {
|
|
84
|
+
throw new QuickModeConfigError(
|
|
85
|
+
`categoryOverrides.${key} must not contain an empty or non-string glob pattern`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (value.length === 0) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
patterns.set(key, Object.freeze([...value]));
|
|
93
|
+
matchers.set(key, Object.freeze(value.map(globToRegExp)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new CategoryOverrideRules(patterns, matchers);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isEmpty(): boolean {
|
|
100
|
+
return this.matchers.size === 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* パスに対応する override カテゴリを返す。
|
|
105
|
+
* 一致が無ければ null(組み込み分類へ委譲)。
|
|
106
|
+
* 複数カテゴリに一致した場合はリスク優先度が最も高いカテゴリを返すため、
|
|
107
|
+
* JSON のキー列挙順に依存しない決定的な結果になる(domain_model.md DD-4)。
|
|
108
|
+
*/
|
|
109
|
+
resolve(filePath: string): ChangeCategory | null {
|
|
110
|
+
if (filePath === '' || this.matchers.size === 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let resolved: ChangeCategory | null = null;
|
|
115
|
+
let resolvedPriority = -1;
|
|
116
|
+
|
|
117
|
+
for (const [category, regexps] of this.matchers) {
|
|
118
|
+
if (!regexps.some((regexp) => regexp.test(filePath))) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const candidate = ChangeCategory.fromString(category);
|
|
122
|
+
const priority = candidate.riskPriority();
|
|
123
|
+
if (priority > resolvedPriority) {
|
|
124
|
+
resolvedPriority = priority;
|
|
125
|
+
resolved = candidate;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return resolved;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** キーは ChangeCategory 語彙順に正規化する(equals をキー順に依存させないため) */
|
|
133
|
+
toRecord(): Readonly<Record<string, readonly string[]>> {
|
|
134
|
+
const record: Record<string, readonly string[]> = {};
|
|
135
|
+
for (const category of CHANGE_CATEGORY_VALUES) {
|
|
136
|
+
const globs = this.patterns.get(category);
|
|
137
|
+
if (globs !== undefined) {
|
|
138
|
+
record[category] = globs;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return Object.freeze(record);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
equals(other: CategoryOverrideRules): boolean {
|
|
145
|
+
return JSON.stringify(this.toRecord()) === JSON.stringify(other.toRecord());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -24,6 +24,31 @@ const VALID_CATEGORIES: readonly ChangeCategoryValue[] = [
|
|
|
24
24
|
'api',
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* ChangeCategory の語彙。config の enum 検証や categoryOverrides の
|
|
29
|
+
* キー検証はこの定義を唯一の権威として参照する。
|
|
30
|
+
* @work-item-id WI-372
|
|
31
|
+
*/
|
|
32
|
+
export const CHANGE_CATEGORY_VALUES: readonly ChangeCategoryValue[] = VALID_CATEGORIES;
|
|
33
|
+
|
|
34
|
+
/** 与えられた文字列が ChangeCategory の語彙に含まれるか(正規化はしない) */
|
|
35
|
+
export function isChangeCategoryValue(raw: string): raw is ChangeCategoryValue {
|
|
36
|
+
return VALID_CATEGORIES.includes(raw as ChangeCategoryValue);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// リスク順優先度(api > domain > feature > bugfix > test > config > docs)。
|
|
40
|
+
// 複数の分類条件に一致した場合に、より高リスク側へ倒すための順序定義。
|
|
41
|
+
// @work-item-id WI-372
|
|
42
|
+
const RISK_PRIORITY: Record<ChangeCategoryValue, number> = {
|
|
43
|
+
api: 6,
|
|
44
|
+
domain: 5,
|
|
45
|
+
feature: 4,
|
|
46
|
+
bugfix: 3,
|
|
47
|
+
test: 2,
|
|
48
|
+
config: 1,
|
|
49
|
+
docs: 0,
|
|
50
|
+
};
|
|
51
|
+
|
|
27
52
|
export class UnknownChangeCategoryError extends Error {
|
|
28
53
|
constructor(raw: string) {
|
|
29
54
|
super(`Unknown change category: "${raw}". Valid values: ${VALID_CATEGORIES.join(', ')}`);
|
|
@@ -50,6 +75,14 @@ export class ChangeCategory {
|
|
|
50
75
|
return this.value === 'domain' || this.value === 'feature' || this.value === 'api';
|
|
51
76
|
}
|
|
52
77
|
|
|
78
|
+
/**
|
|
79
|
+
* リスク優先度。値が大きいほど高リスク(api=6 ... docs=0)。
|
|
80
|
+
* @work-item-id WI-372
|
|
81
|
+
*/
|
|
82
|
+
riskPriority(): number {
|
|
83
|
+
return RISK_PRIORITY[this.value];
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
toString(): string {
|
|
54
87
|
return this.value;
|
|
55
88
|
}
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* Quick Mode設定を表す値オブジェクト
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
8
|
+
import { QuickModeConfigError } from '../errors/quick-mode-config-error.js';
|
|
9
|
+
import {
|
|
10
|
+
CategoryOverrideRules,
|
|
11
|
+
type CategoryOverrideRulesInput,
|
|
12
|
+
} from './category-override-rules.js';
|
|
13
|
+
import { CHANGE_CATEGORY_VALUES, isChangeCategoryValue } from './change-category.js';
|
|
14
|
+
|
|
15
|
+
export { QuickModeConfigError };
|
|
14
16
|
|
|
15
17
|
export type FullModeRequiredRuleId = 'mixedCategories' | 'newDomainFile' | 'apiContractChange';
|
|
16
18
|
|
|
@@ -32,17 +34,21 @@ export class QuickModeConfig {
|
|
|
32
34
|
readonly maintainedLayers: readonly string[];
|
|
33
35
|
readonly relaxedGates: readonly string[];
|
|
34
36
|
readonly fullModeRequiredWhen: FullModeRequiredRules;
|
|
37
|
+
/** @work-item-id WI-372 プロジェクト固有パスのカテゴリ割当ルール(未設定時は空) */
|
|
38
|
+
readonly categoryOverrides: CategoryOverrideRules;
|
|
35
39
|
|
|
36
40
|
private constructor(
|
|
37
41
|
allowedCategories: readonly string[],
|
|
38
42
|
maintainedLayers: readonly string[],
|
|
39
43
|
relaxedGates: readonly string[],
|
|
40
|
-
fullModeRequiredWhen: FullModeRequiredRules
|
|
44
|
+
fullModeRequiredWhen: FullModeRequiredRules,
|
|
45
|
+
categoryOverrides: CategoryOverrideRules
|
|
41
46
|
) {
|
|
42
47
|
this.allowedCategories = allowedCategories;
|
|
43
48
|
this.maintainedLayers = maintainedLayers;
|
|
44
49
|
this.relaxedGates = relaxedGates;
|
|
45
50
|
this.fullModeRequiredWhen = fullModeRequiredWhen;
|
|
51
|
+
this.categoryOverrides = categoryOverrides;
|
|
46
52
|
Object.freeze(this);
|
|
47
53
|
}
|
|
48
54
|
|
|
@@ -51,13 +57,26 @@ export class QuickModeConfig {
|
|
|
51
57
|
maintainedLayers: string[];
|
|
52
58
|
relaxedGates: string[];
|
|
53
59
|
fullModeRequiredWhen?: Partial<FullModeRequiredRules>;
|
|
60
|
+
categoryOverrides?: CategoryOverrideRulesInput;
|
|
54
61
|
}): QuickModeConfig {
|
|
55
|
-
const { allowedCategories, maintainedLayers, relaxedGates, fullModeRequiredWhen } = raw;
|
|
62
|
+
const { allowedCategories, maintainedLayers, relaxedGates, fullModeRequiredWhen, categoryOverrides } = raw;
|
|
56
63
|
|
|
57
64
|
if (allowedCategories.length === 0) {
|
|
58
65
|
throw new QuickModeConfigError('allowedCategories must not be empty');
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
// WI-373: allowedCategories は ChangeCategory 7 値の enum。
|
|
69
|
+
// 従来は非空チェックのみだったため "typoo" のような未知値が黙って通り、
|
|
70
|
+
// 「設定したのに効かない」状態になっていた。分類結果のキーは常に小文字なので
|
|
71
|
+
// 大文字小文字の正規化はせず厳密一致で拒否する(正規化は効かない設定の黙認になる)。
|
|
72
|
+
for (const category of allowedCategories) {
|
|
73
|
+
if (!isChangeCategoryValue(category)) {
|
|
74
|
+
throw new QuickModeConfigError(
|
|
75
|
+
`allowedCategories contains unknown category "${category}". Valid values: ${CHANGE_CATEGORY_VALUES.join(', ')}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
const mergedRules: FullModeRequiredRules = Object.freeze({
|
|
62
81
|
mixedCategories: fullModeRequiredWhen?.mixedCategories ?? DEFAULT_FULL_MODE_REQUIRED_WHEN.mixedCategories,
|
|
63
82
|
newDomainFile: fullModeRequiredWhen?.newDomainFile ?? DEFAULT_FULL_MODE_REQUIRED_WHEN.newDomainFile,
|
|
@@ -68,7 +87,8 @@ export class QuickModeConfig {
|
|
|
68
87
|
Object.freeze([...allowedCategories]),
|
|
69
88
|
Object.freeze([...maintainedLayers]),
|
|
70
89
|
Object.freeze([...relaxedGates]),
|
|
71
|
-
mergedRules
|
|
90
|
+
mergedRules,
|
|
91
|
+
CategoryOverrideRules.create(categoryOverrides)
|
|
72
92
|
);
|
|
73
93
|
}
|
|
74
94
|
|
|
@@ -100,7 +120,8 @@ export class QuickModeConfig {
|
|
|
100
120
|
JSON.stringify(this.relaxedGates) === JSON.stringify(other.relaxedGates) &&
|
|
101
121
|
this.fullModeRequiredWhen.mixedCategories === other.fullModeRequiredWhen.mixedCategories &&
|
|
102
122
|
this.fullModeRequiredWhen.newDomainFile === other.fullModeRequiredWhen.newDomainFile &&
|
|
103
|
-
this.fullModeRequiredWhen.apiContractChange === other.fullModeRequiredWhen.apiContractChange
|
|
123
|
+
this.fullModeRequiredWhen.apiContractChange === other.fullModeRequiredWhen.apiContractChange &&
|
|
124
|
+
this.categoryOverrides.equals(other.categoryOverrides)
|
|
104
125
|
);
|
|
105
126
|
}
|
|
106
127
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// @unit quick-mode
|
|
2
2
|
// @layer application
|
|
3
3
|
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
CHANGE_CATEGORY_VALUES,
|
|
6
|
+
ChangeCategory,
|
|
7
|
+
isChangeCategoryValue,
|
|
8
|
+
UnknownChangeCategoryError,
|
|
9
|
+
} from './domain/value-objects/change-category.js';
|
|
10
|
+
export { CategoryOverrideRules } from './domain/value-objects/category-override-rules.js';
|
|
5
11
|
export { ChangedFile } from './domain/value-objects/changed-file.js';
|
|
6
12
|
export { ChangeClassification } from './domain/value-objects/change-classification.js';
|
|
7
13
|
export { QuickModeConfig, QuickModeConfigError } from './domain/value-objects/quick-mode-config.js';
|
|
@@ -2,81 +2,132 @@
|
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit quick-mode
|
|
4
4
|
* @work-item-id WI-140
|
|
5
|
+
* @work-item-id WI-372
|
|
6
|
+
* @work-item-id WI-377
|
|
5
7
|
*
|
|
6
8
|
* phasegate.config.json から QuickModeConfig を取得する Adapter
|
|
9
|
+
*
|
|
10
|
+
* ADR-040: 実効値は config-foundation の防御プリセット解決を経由して決定する。
|
|
11
|
+
* preset 定義(presets/*.json の quickMode)が実効値の所在であり、
|
|
12
|
+
* 下の DEFAULT_QUICK_MODE_CONFIG は preset 解決不能時の fail-open 用フォールバックである。
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
|
-
import * as fs from
|
|
10
|
-
import * as path from
|
|
11
|
-
import {
|
|
15
|
+
import * as fs from "node:fs/promises";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import type {
|
|
18
|
+
HarnessConfigResolvedDocument,
|
|
19
|
+
HarnessConfigSourceDocument,
|
|
20
|
+
PresetId,
|
|
21
|
+
} from "../../../config-foundation/domain/harness-config.js";
|
|
22
|
+
import { PresetResolutionService } from "../../../config-foundation/domain/services/preset-resolution-service.js";
|
|
23
|
+
import { PresetDefinitionStore } from "../../../config-foundation/infrastructure/preset-definition-store.js";
|
|
24
|
+
import { QuickModeConfig } from "../../domain/value-objects/quick-mode-config.js";
|
|
12
25
|
|
|
13
26
|
export class HarnessConfigNotFoundError extends Error {
|
|
14
27
|
constructor(filePath: string) {
|
|
15
28
|
super(`phasegate.config.json not found: ${filePath}`);
|
|
16
|
-
this.name =
|
|
29
|
+
this.name = "HarnessConfigNotFoundError";
|
|
17
30
|
}
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
export class HarnessConfigParseError extends Error {
|
|
21
34
|
constructor(message: string) {
|
|
22
35
|
super(`Failed to parse phasegate.config.json: ${message}`);
|
|
23
|
-
this.name =
|
|
36
|
+
this.name = "HarnessConfigParseError";
|
|
24
37
|
}
|
|
25
38
|
}
|
|
26
39
|
|
|
40
|
+
/**
|
|
41
|
+
* preset 解決が使えない config(未知 preset / project 欠落 / 他セクション不正)でも
|
|
42
|
+
* Quick Mode 判定を止めないためのフォールバック。ADR-038 §3-1 の fail-open 原則に従い、
|
|
43
|
+
* preset 解決の導入によって新たな遮断経路を作らない。
|
|
44
|
+
*/
|
|
27
45
|
const DEFAULT_QUICK_MODE_CONFIG = {
|
|
28
|
-
allowedCategories: [
|
|
29
|
-
maintainedLayers: [
|
|
30
|
-
relaxedGates: [
|
|
46
|
+
allowedCategories: ["bugfix", "docs", "test", "config"],
|
|
47
|
+
maintainedLayers: ["L1", "L2-002", "L2-003", "L2-014", "L3-001"],
|
|
48
|
+
relaxedGates: ["L2-001", "L3-002", "L3-003", "L3-004", "L4"],
|
|
31
49
|
};
|
|
32
50
|
|
|
51
|
+
const PRESET_IDS: readonly PresetId[] = ["minimal", "standard", "strict"];
|
|
52
|
+
|
|
53
|
+
type RawQuickMode = HarnessConfigResolvedDocument["quickMode"];
|
|
54
|
+
|
|
55
|
+
function isPresetId(value: unknown): value is PresetId {
|
|
56
|
+
return typeof value === "string" && PRESET_IDS.includes(value as PresetId);
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
export class HarnessConfigQuickModeConfigAdapter {
|
|
34
60
|
private readonly configPath: string;
|
|
61
|
+
private readonly presetDefinitionStore: PresetDefinitionStore;
|
|
62
|
+
private readonly presetResolutionService: PresetResolutionService;
|
|
35
63
|
|
|
36
64
|
constructor(configPath?: string) {
|
|
37
|
-
this.configPath = configPath ?? path.resolve(process.cwd(),
|
|
65
|
+
this.configPath = configPath ?? path.resolve(process.cwd(), "phasegate.config.json");
|
|
66
|
+
this.presetDefinitionStore = new PresetDefinitionStore();
|
|
67
|
+
this.presetResolutionService = new PresetResolutionService();
|
|
38
68
|
}
|
|
39
69
|
|
|
40
70
|
async getQuickModeConfig(): Promise<QuickModeConfig> {
|
|
71
|
+
const parsed = await this.readSourceDocument();
|
|
72
|
+
const quickMode = this.resolveQuickMode(parsed);
|
|
73
|
+
|
|
74
|
+
return QuickModeConfig.create({
|
|
75
|
+
allowedCategories: quickMode?.allowedCategories ?? DEFAULT_QUICK_MODE_CONFIG.allowedCategories,
|
|
76
|
+
maintainedLayers: quickMode?.maintainedLayers ?? DEFAULT_QUICK_MODE_CONFIG.maintainedLayers,
|
|
77
|
+
relaxedGates: quickMode?.relaxedGates ?? DEFAULT_QUICK_MODE_CONFIG.relaxedGates,
|
|
78
|
+
fullModeRequiredWhen: quickMode?.fullModeRequiredWhen,
|
|
79
|
+
// WI-372 × WI-377 合流点: categoryOverrides は preset 解決経路と raw 縮退経路の
|
|
80
|
+
// 双方が返す `quickMode` から一様に読む。preset 定義は当該キーを宣言しないため
|
|
81
|
+
// deepMerge は source の宣言をそのまま引き継ぎ(base 側 undefined → override を clone)、
|
|
82
|
+
// preset 解決が例外で縮退した場合も raw の宣言がそのまま渡る。未設定時は空ルール
|
|
83
|
+
// = 現行分類のまま。enum 違反は QuickModeConfig.create が QuickModeConfigError を
|
|
84
|
+
// 投げ、この try/catch の外側なので fail-closed 経路へ表面化する。
|
|
85
|
+
categoryOverrides: quickMode?.categoryOverrides,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async readSourceDocument(): Promise<Record<string, unknown>> {
|
|
41
90
|
let content: string;
|
|
42
91
|
|
|
43
92
|
try {
|
|
44
|
-
content = await fs.readFile(this.configPath,
|
|
93
|
+
content = (await fs.readFile(this.configPath, "utf8")) as string;
|
|
45
94
|
} catch (err) {
|
|
46
95
|
const error = err as NodeJS.ErrnoException;
|
|
47
|
-
if (error.code ===
|
|
96
|
+
if (error.code === "ENOENT") {
|
|
48
97
|
throw new HarnessConfigNotFoundError(this.configPath);
|
|
49
98
|
}
|
|
50
99
|
throw err;
|
|
51
100
|
}
|
|
52
101
|
|
|
53
|
-
let parsed: Record<string, unknown>;
|
|
54
102
|
try {
|
|
55
|
-
|
|
103
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
56
104
|
} catch (err) {
|
|
57
105
|
throw new HarnessConfigParseError(err instanceof Error ? err.message : String(err));
|
|
58
106
|
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 防御プリセット解決を経由した quickMode を返す。
|
|
111
|
+
* preset が特定できない、または解決が失敗する config では raw の quickMode に縮退し、
|
|
112
|
+
* 未宣言キーは呼び出し側で DEFAULT_QUICK_MODE_CONFIG に補完される。
|
|
113
|
+
*/
|
|
114
|
+
private resolveQuickMode(parsed: Record<string, unknown>): Partial<RawQuickMode> | undefined {
|
|
115
|
+
const rawQuickMode = parsed["quickMode"] as Partial<RawQuickMode> | undefined;
|
|
116
|
+
const preset = (parsed["project"] as { preset?: unknown } | undefined)?.preset;
|
|
59
117
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
maintainedLayers?: string[];
|
|
63
|
-
relaxedGates?: string[];
|
|
64
|
-
fullModeRequiredWhen?: {
|
|
65
|
-
mixedCategories?: boolean;
|
|
66
|
-
newDomainFile?: boolean;
|
|
67
|
-
apiContractChange?: boolean;
|
|
68
|
-
};
|
|
69
|
-
} | undefined;
|
|
70
|
-
|
|
71
|
-
if (!quickMode) {
|
|
72
|
-
return QuickModeConfig.create(DEFAULT_QUICK_MODE_CONFIG);
|
|
118
|
+
if (!isPresetId(preset)) {
|
|
119
|
+
return rawQuickMode;
|
|
73
120
|
}
|
|
74
121
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
122
|
+
try {
|
|
123
|
+
const presetDefinitions = this.presetDefinitionStore.load();
|
|
124
|
+
const resolved = this.presetResolutionService.resolve(
|
|
125
|
+
parsed as unknown as HarnessConfigSourceDocument,
|
|
126
|
+
presetDefinitions[preset],
|
|
127
|
+
);
|
|
128
|
+
return resolved.quickMode;
|
|
129
|
+
} catch {
|
|
130
|
+
return rawQuickMode;
|
|
131
|
+
}
|
|
81
132
|
}
|
|
82
133
|
}
|
|
@@ -35,6 +35,18 @@ languages: [typescript]
|
|
|
35
35
|
### 出力ファイル
|
|
36
36
|
`docs/inception/_shared/product_overview_plan.md`
|
|
37
37
|
|
|
38
|
+
> **テンプレート実体の取得(正本)**: 下の構成ブロックは読み取り用の要約であり、
|
|
39
|
+
> 実体は `templates/product_overview_plan.template.md` にある。
|
|
40
|
+
> node_modules を Read せずに取得・生成するには次を使う:
|
|
41
|
+
>
|
|
42
|
+
> ```bash
|
|
43
|
+
> phasegate templates show product_overview_plan # 本文を stdout に出力
|
|
44
|
+
> phasegate scaffold-inception --kind product-overview-plan --apply # 実ファイルを生成
|
|
45
|
+
> ```
|
|
46
|
+
>
|
|
47
|
+
> scaffold した文書は無編集で Level-1 フェーズゲート(`interactive` モード)を通る。
|
|
48
|
+
> `[Answer]` は **人間が記入する**。AI が埋めると承認証跡の偽造になる。
|
|
49
|
+
|
|
38
50
|
### 計画ファイルの構成
|
|
39
51
|
|
|
40
52
|
```markdown
|
|
@@ -93,6 +105,13 @@ languages: [typescript]
|
|
|
93
105
|
|------|--------|
|
|
94
106
|
| 成果物 | `docs/product/product_overview.md` |
|
|
95
107
|
|
|
108
|
+
> **テンプレート実体の取得(正本)**: 実体は `templates/product_overview.template.md` にある。
|
|
109
|
+
>
|
|
110
|
+
> ```bash
|
|
111
|
+
> phasegate templates show product_overview
|
|
112
|
+
> phasegate scaffold-inception --kind product-overview --apply
|
|
113
|
+
> ```
|
|
114
|
+
|
|
96
115
|
### product_overview.md の構成
|
|
97
116
|
|
|
98
117
|
```markdown
|
|
@@ -81,6 +81,17 @@ MVPスコープの方針・分類基準・不明点を整理し、人間の承
|
|
|
81
81
|
### 出力ファイル
|
|
82
82
|
`docs/inception/_shared/story_mapping_plan.md`
|
|
83
83
|
|
|
84
|
+
> **テンプレート実体の取得(正本)**: 下の構成ブロックは読み取り用の要約であり、
|
|
85
|
+
> 実体は `templates/story_mapping_plan.template.md` にある。
|
|
86
|
+
> node_modules を Read せずに取得・生成するには次を使う:
|
|
87
|
+
>
|
|
88
|
+
> ```bash
|
|
89
|
+
> phasegate templates show story_mapping_plan
|
|
90
|
+
> phasegate scaffold-inception --kind story-mapping-plan --apply
|
|
91
|
+
> ```
|
|
92
|
+
>
|
|
93
|
+
> `[Answer]` は **人間が記入する**。AI が埋めると承認証跡の偽造になる。
|
|
94
|
+
|
|
84
95
|
### 計画ファイルの構成
|
|
85
96
|
|
|
86
97
|
```markdown
|
|
@@ -45,6 +45,17 @@ If not, halt and ask the user to create the WI first, or offer to run `phasegate
|
|
|
45
45
|
### 出力ファイル
|
|
46
46
|
`docs/inception/_shared/story_writer_plan.md`
|
|
47
47
|
|
|
48
|
+
> **テンプレート実体の取得(正本)**: 下の構成ブロックは読み取り用の要約であり、
|
|
49
|
+
> 実体は `templates/story_writer_plan.template.md` にある。
|
|
50
|
+
> node_modules を Read せずに取得・生成するには次を使う:
|
|
51
|
+
>
|
|
52
|
+
> ```bash
|
|
53
|
+
> phasegate templates show story_writer_plan
|
|
54
|
+
> phasegate scaffold-inception --kind story-writer-plan --apply
|
|
55
|
+
> ```
|
|
56
|
+
>
|
|
57
|
+
> `[Answer]` は **人間が記入する**。AI が埋めると承認証跡の偽造になる。
|
|
58
|
+
|
|
48
59
|
### 計画ファイルの構成
|
|
49
60
|
|
|
50
61
|
```markdown
|
|
@@ -84,6 +84,17 @@ Unit分割の方針・グルーピングの根拠・不明点を整理し、人
|
|
|
84
84
|
|
|
85
85
|
> **パス注記**: 本スキルが扱う設計文書パス(`docs/inception/...` / `docs/product/units/...`)は既定値であり、consumer が `phasegate.config.json` の `paths` 設定で上書きしている場合はそちらが優先される。
|
|
86
86
|
|
|
87
|
+
> **テンプレート実体の取得(正本)**: 下の構成ブロックは読み取り用の要約であり、
|
|
88
|
+
> 実体は `templates/unit_design_plan.template.md` にある。
|
|
89
|
+
> node_modules を Read せずに取得・生成するには次を使う:
|
|
90
|
+
>
|
|
91
|
+
> ```bash
|
|
92
|
+
> phasegate templates show unit_design_plan
|
|
93
|
+
> phasegate scaffold-inception --kind unit-design-plan --apply
|
|
94
|
+
> ```
|
|
95
|
+
>
|
|
96
|
+
> `[Answer]` は **人間が記入する**。AI が埋めると承認証跡の偽造になる。
|
|
97
|
+
|
|
87
98
|
### 計画ファイルの構成
|
|
88
99
|
|
|
89
100
|
```markdown
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# <プロダクト名>
|
|
2
|
+
|
|
3
|
+
> **スキル**: `product-architect` Phase 2(実行)
|
|
4
|
+
> **入力**: `docs/inception/_shared/product_overview_plan.md`(承認済み)
|
|
5
|
+
> **作成日**: <YYYY-MM-DD>
|
|
6
|
+
|
|
7
|
+
このファイルは `phasegate scaffold-inception --kind product-overview --apply`
|
|
8
|
+
が生成した雛形です。TODO を実体で埋めてください。
|
|
9
|
+
このドキュメントは全スキルの**前提**として参照されます。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 1. プロダクト定義
|
|
14
|
+
|
|
15
|
+
### 1.1 根本思想
|
|
16
|
+
|
|
17
|
+
TODO: このプロダクトが何を信条として作られるかを 2〜3 行で記述
|
|
18
|
+
|
|
19
|
+
### 1.2 解決する問題
|
|
20
|
+
|
|
21
|
+
TODO: 現状の痛みと、それが放置された場合のコスト
|
|
22
|
+
|
|
23
|
+
### 1.3 対象ユーザー
|
|
24
|
+
|
|
25
|
+
TODO: 主ターゲットと、明示的に対象外とするユーザー
|
|
26
|
+
|
|
27
|
+
## 2. コアドメイン
|
|
28
|
+
|
|
29
|
+
### 2.1 主要概念(ユビキタス言語)
|
|
30
|
+
|
|
31
|
+
| 概念 | 定義 | 例 |
|
|
32
|
+
|------|------|-----|
|
|
33
|
+
| TODO | TODO | TODO |
|
|
34
|
+
|
|
35
|
+
### 2.2 概念間の関係図
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
graph TD
|
|
39
|
+
A[TODO] --> B[TODO]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 3. アーキテクチャ仕様
|
|
43
|
+
|
|
44
|
+
### 3.1 全体構造(レイヤー図)
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
TODO: レイヤー構成
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3.2 技術スタック
|
|
51
|
+
|
|
52
|
+
| レイヤー | 技術 | 選定理由 |
|
|
53
|
+
|---------|------|---------|
|
|
54
|
+
| TODO | TODO | TODO |
|
|
55
|
+
|
|
56
|
+
### 3.3 分離方針
|
|
57
|
+
|
|
58
|
+
TODO: 認証/認可、同期/非同期 等の分離方針
|
|
59
|
+
|
|
60
|
+
## 4. ADR Summary(技術決定記録)
|
|
61
|
+
|
|
62
|
+
| # | 決定事項 | 選択肢 | 決定 | 理由 |
|
|
63
|
+
|---|---------|-------|------|------|
|
|
64
|
+
| 1 | TODO | TODO | TODO | TODO |
|
|
65
|
+
|
|
66
|
+
## 5. 制約
|
|
67
|
+
|
|
68
|
+
### 5.1 運用上の制約
|
|
69
|
+
|
|
70
|
+
TODO
|
|
71
|
+
|
|
72
|
+
### 5.2 実装上の制約
|
|
73
|
+
|
|
74
|
+
TODO
|
|
75
|
+
|
|
76
|
+
### 5.3 パフォーマンス要件
|
|
77
|
+
|
|
78
|
+
TODO
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 注意事項
|
|
83
|
+
|
|
84
|
+
- ユビキタス言語はプロジェクト全体で一貫して使用する
|
|
85
|
+
- 技術選定の「なぜ」を必ず ADR Summary に記録する
|