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.
Files changed (56) hide show
  1. package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
  2. package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
  3. package/docs/guide/cli-reference.md +89 -7
  4. package/docs/guide/configuration.md +43 -2
  5. package/docs/guide/quick-vs-full-mode.md +36 -1
  6. package/package.json +2 -2
  7. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
  8. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +16 -8
  9. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +190 -2
  10. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
  11. package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
  12. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +6 -3
  13. package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
  14. package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
  15. package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
  16. package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
  17. package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
  18. package/scripts/harness/ci-governance/composition-root.ts +55 -3
  19. package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
  20. package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
  21. package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
  22. package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
  23. package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
  24. package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
  25. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
  26. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
  27. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
  28. package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
  29. package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
  30. package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
  31. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -2
  32. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -2
  33. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -2
  34. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
  35. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
  36. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
  37. package/scripts/harness/main.ts +102 -12
  38. package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
  39. package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
  40. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +4 -1
  41. package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
  42. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +46 -17
  43. package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
  44. package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
  45. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
  46. package/scripts/harness/quick-mode/index.ts +7 -1
  47. package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
  48. package/skills/product-architect/SKILL.md +19 -0
  49. package/skills/story-mapper/SKILL.md +11 -0
  50. package/skills/story-writer/SKILL.md +11 -0
  51. package/skills/unit-designer/SKILL.md +11 -0
  52. package/templates/product_overview.template.md +85 -0
  53. package/templates/product_overview_plan.template.md +55 -0
  54. package/templates/story_mapping_plan.template.md +61 -0
  55. package/templates/story_writer_plan.template.md +61 -0
  56. 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
- export class QuickModeConfigError extends Error {
9
- constructor(message: string) {
10
- super(message);
11
- this.name = 'QuickModeConfigError';
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 { ChangeCategory, UnknownChangeCategoryError } from './domain/value-objects/change-category.js';
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 'node:fs/promises';
10
- import * as path from 'node:path';
11
- import { QuickModeConfig } from '../../domain/value-objects/quick-mode-config.js';
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 = 'HarnessConfigNotFoundError';
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 = 'HarnessConfigParseError';
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: ['bugfix', 'docs', 'test', 'config'],
29
- maintainedLayers: ['L1', 'L2-002', 'L2-003', 'L2-014', 'L3-001'],
30
- relaxedGates: ['L2-001', 'L3-002', 'L3-003', 'L3-004', 'L4'],
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(), 'phasegate.config.json');
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, 'utf8') as string;
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 === 'ENOENT') {
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
- parsed = JSON.parse(content) as Record<string, unknown>;
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
- const quickMode = parsed['quickMode'] as {
61
- allowedCategories?: string[];
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
- return QuickModeConfig.create({
76
- allowedCategories: quickMode.allowedCategories ?? DEFAULT_QUICK_MODE_CONFIG.allowedCategories,
77
- maintainedLayers: quickMode.maintainedLayers ?? DEFAULT_QUICK_MODE_CONFIG.maintainedLayers,
78
- relaxedGates: quickMode.relaxedGates ?? DEFAULT_QUICK_MODE_CONFIG.relaxedGates,
79
- fullModeRequiredWhen: quickMode.fullModeRequiredWhen,
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 に記録する