phasegate 0.114.0 → 0.117.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/docs/ADR/ADR-016-paths-config-placeholder.md +155 -0
- package/docs/guide/configuration.md +23 -0
- package/package.json +1 -1
- package/scripts/harness/config-foundation/application/mappers/phase-config-section-mapper.ts +1 -0
- package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +13 -2
- package/scripts/harness/phase-dependency-model/application/services/phase-info-resolver.ts +3 -1
- package/scripts/harness/phase-dependency-model/application/usecases/get-phase-info-usecase.ts +1 -0
- package/scripts/harness/phase-dependency-model/domain/definitions/full-phase-nodes.ts +23 -23
- package/scripts/harness/phase-dependency-model/domain/definitions/minimal-phase-nodes.ts +3 -3
- package/scripts/harness/phase-dependency-model/domain/definitions/standard-phase-nodes.ts +13 -13
- package/scripts/harness/phase-dependency-model/domain/models/phase-structure.ts +7 -4
- package/scripts/harness/phase-dependency-model/domain/ports/artifact-existence-checker-port.ts +2 -1
- package/scripts/harness/phase-dependency-model/domain/ports/phase-config-provider-port.ts +2 -0
- package/scripts/harness/phase-dependency-model/domain/values/artifact.ts +39 -4
- package/scripts/harness/phase-dependency-model/infrastructure/config/harness-config-phase-config-provider.ts +18 -0
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-artifact-existence-checker.ts +3 -2
- package/scripts/harness/skill-quality/infrastructure/adapters/git-commit-executor-adapter.ts +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.117.0] - 2026-05-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-085: phase-gate validator (L2-001) が `paths.designDocs` / `paths.inceptionDocs` 設定を尊重しない問題を修正** — 外部レポーター nakataj-mti が GitHub Issue [#1](https://github.com/junpei-9898/phasegate/issues/1) で報告。`paths.designDocs` を `mydocs/...` などのデフォルト外パスに変更しても、L2-001 が要求する成果物パスは `docs/inception/...` / `docs/product/...` のままになる挙動を修正した。`docs/guide/configuration.md` の「`paths` を変えれば全 validator / hook が解決する」記述と実装の齟齬を解消。
|
|
15
|
+
- **新プレースホルダ**: `Artifact.path` で `{designDocsRoot}` / `{inceptionDocsRoot}` を許可。`Artifact.resolve(scope, pathRoots?)` が `paths.designDocs` / `paths.inceptionDocs` の値(未指定時はデフォルト `docs/product/construction` / `docs/inception`)でこれらを展開するよう拡張([ADR-016](docs/ADR/ADR-016-paths-config-placeholder.md))。
|
|
16
|
+
- **`docs/` 接頭辞バリデーション撤廃**: `Artifact.create()` の `path` が `docs/` 始まりでなければ `InvalidArtifactPathError` を投げる挙動を削除。プレースホルダ展開後の任意 root(`mydocs/...` 等)を許容するため。許可外プレースホルダの拒否は維持。
|
|
17
|
+
- **新ポートメソッド**: `PhaseConfigProviderPort.getPathRoots(): Promise<{ designDocsRoot, inceptionDocsRoot }>` を追加。`HarnessConfigPhaseConfigProvider` が `paths.designDocs` / `paths.inceptionDocs` 設定を読み(末尾スラッシュは trim)、未指定時はデフォルト値を返す。
|
|
18
|
+
- **流入経路**: `EvidenceBundleAssembler.assembleForLevel` 内で `getPathRoots()` を 1 回呼び、結果を `Artifact.resolve(scope, pathRoots)` および `ArtifactExistenceCheckerPort.checkAll(artifacts, scope, pathRoots)` に渡す。`PhaseInfoResolver.resolve(..., pathRoots?)` でブロッカー表示時にも root プレースホルダを実値に展開。
|
|
19
|
+
- **phase-nodes リテラル置換**: `STANDARD_PHASE_NODES` / `FULL_PHASE_NODES` / `MINIMAL_PHASE_NODES` の path リテラル `'docs/inception/...'` / `'docs/product/construction/...'` を `'{inceptionDocsRoot}/...'` / `'{designDocsRoot}/...'` に置換。
|
|
20
|
+
- **後方互換**: `paths` 未指定または既定値の場合、要求パスは v0.116.0 以前と完全同一。
|
|
21
|
+
- **スコープ外(Q1 (α) 採用)**: `docs/product/product_overview.md` / `docs/product/user_stories.md` / `docs/product/units/{unit}_unit.md` 等、`docs/product/` 直下の Level 1 product-wide 成果物はリテラル維持。`paths.designDocs`(= `docs/product/construction`)は construction subtree のみを管理する。
|
|
22
|
+
- **テスト追加**: ユニット 9 ケース(UT-PD-169〜177)と既存 5 ケースの意味反転 / シグネチャ拡張、IT 8 ケース(IT-PD-123〜130)を追加。`unit_test_design.md` / `it_test_design.md` / `coverage_report.md` 同期更新。
|
|
23
|
+
- **ドキュメント整合**: `docs/guide/configuration.md` §「Paths and the AIDLC Document Structure」に新プレースホルダ仕様と非 construction パスの取り扱いを明記。
|
|
24
|
+
|
|
25
|
+
## [0.116.0] - 2026-05-07
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
- **WI-036: skill-quality の git-commit-executor adapter におけるコマンドインジェクション (HIGH) を修正** — WI-035 Phase 3 横展開監査で発見した follow-up。`execSync(\`git commit -m ${JSON.stringify(message)}\`)` を `execFileSync('git', ['commit', '-m', message], ...)` 配列引数形式に置換。`JSON.stringify` がバッククオート / `$` をエスケープせず `/bin/sh -c` 経由で評価される経路を遮断。
|
|
30
|
+
- `scripts/harness/skill-quality/infrastructure/adapters/git-commit-executor-adapter.ts` を `execFileSync` 化、コンストラクタに `gitExecutor` を DI 可能化(テスタビリティ向上、`CommitExecutorPort.commit()` の外部 signature は不変)。
|
|
31
|
+
- 悪意ある `description`(バッククオート / `$()` / `;` / `"` / 改行 / `|` 含む)が引数配列にそのまま渡されシェル評価されないことを assert する unit test (`__tests__/unit/skill-quality/git-commit-executor-adapter.test.ts`) を新規追加。
|
|
32
|
+
- 横展開監査 (再走査): 非テストコード `scripts/harness/**/*.ts` の `execSync(\`...${var}...\`)` パターンが **完全消滅**(WI-035 / WI-036 で網羅完了)。
|
|
33
|
+
- WI-035 / WI-036 の `status` を `drafted` → `implemented` に更新。
|
|
34
|
+
|
|
35
|
+
## [0.115.0] - 2026-05-07
|
|
36
|
+
|
|
37
|
+
### Documentation
|
|
38
|
+
|
|
39
|
+
- **WI-036 起票** — WI-035 Phase 3 横展開監査の follow-up として、`scripts/harness/skill-quality/infrastructure/adapters/git-commit-executor-adapter.ts:14` の `execSync(\`git commit -m ${JSON.stringify(message)}\`)` を同種コマンドインジェクション (HIGH) の sink として切り出し、別 WI に分離。`JSON.stringify` がバッククオート / `$` をエスケープしないため、`description` が外部入力に由来すると `/bin/sh -c` 経由で評価され得る点を文書化。修正実装は次回(`execFileSync` 配列引数化 + DI seed 化 + 悪意 description fixture テスト)。
|
|
40
|
+
- WI-035 description.md の Phase 3 監査結果欄を「WI-036 として起票済み」に更新。
|
|
41
|
+
|
|
10
42
|
## [0.114.0] - 2026-05-07
|
|
11
43
|
|
|
12
44
|
### Security
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# ADR-016: phase-gate validator のパス解決を `paths` config プレースホルダ化する
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted — 2026-05-07
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
`phasegate.config.json` には `paths.designDocs` / `paths.inceptionDocs` という設定が存在し、`docs/guide/configuration.md:471` には次の記述がある:
|
|
10
|
+
|
|
11
|
+
> If you move your design documents to a non-default location, update `paths` accordingly so that all validators and hooks resolve files correctly.
|
|
12
|
+
|
|
13
|
+
しかし実装では、L2-001(phase-gate validator)の経路で `paths.*` 設定が反映されず、利用者が `paths.designDocs: "mydocs/product/construction"` 等を設定しても **要求される成果物パスは `docs/inception/...` / `docs/product/...` のまま** となる。外部レポーター(GitHub Issue [#1](https://github.com/junpei-9898/phasegate/issues/1), nakataj-mti, 2026-05-07)が報告し、phasegate チームによる再検証で報告内容が正確であることを確認した(WI-085)。
|
|
14
|
+
|
|
15
|
+
### バグの実体(grep ベース、2026-05-07)
|
|
16
|
+
|
|
17
|
+
#### 1. ハードコードされたパスリテラル
|
|
18
|
+
|
|
19
|
+
`scripts/harness/phase-dependency-model/domain/definitions/standard-phase-nodes.ts` 全 18 か所の `path:` フィールドが `'docs/inception/...'` / `'docs/product/...'` 文字列リテラル。`full-phase-nodes.ts` / `minimal-phase-nodes.ts` も同様の構造。
|
|
20
|
+
|
|
21
|
+
#### 2. プレースホルダ展開の制約
|
|
22
|
+
|
|
23
|
+
`scripts/harness/phase-dependency-model/domain/values/artifact.ts:6`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
const ALLOWED_PLACEHOLDERS = new Set(['unit', 'storyId']);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`{designDocsRoot}` / `{inceptionDocsRoot}` のような **トップレベルパス展開は未実装**。
|
|
30
|
+
|
|
31
|
+
#### 3. `docs/` 接頭辞の強制バリデーション
|
|
32
|
+
|
|
33
|
+
`scripts/harness/phase-dependency-model/domain/values/artifact.ts:48`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
if (normalizedPath.length === 0 || !normalizedPath.startsWith('docs/')) {
|
|
37
|
+
throw new InvalidArtifactPathError(args.path);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
仮にユーザーが `phaseDependencies.preset: "custom"` + `gates[]` で迂回しようとしても、`mydocs/...` のような非 `docs/` 接頭辞は **Artifact 生成時点で拒否される**。
|
|
42
|
+
|
|
43
|
+
#### 4. `paths` config が `Artifact.resolve()` に到達していない
|
|
44
|
+
|
|
45
|
+
`scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts:42-49` で `phaseConfigProvider` から `planningMode` のみ取得。`paths.designDocs` / `paths.inceptionDocs` は `Artifact.resolve(scope)` 呼び出しに渡されていない。`scope` 型も `{ unitId?: string; storyId?: string }` のみで、ルートパスを通す経路がない。
|
|
46
|
+
|
|
47
|
+
#### 5. 部分対応箇所との不整合
|
|
48
|
+
|
|
49
|
+
`paths.designDocs` を実際に参照している箇所は存在するが、いずれも phase-gate validator(L2-001)の経路ではない:
|
|
50
|
+
|
|
51
|
+
- `scripts/harness/harness-api/infrastructure/adapters/file-system-artifact-scanner-adapter.ts:118`(harness-api の scanner)
|
|
52
|
+
- `scripts/harness/agent-integration/infrastructure/adapters/phase-gate-query-adapter.ts:60`(agent runtime hook の `unitDir` 解決)
|
|
53
|
+
|
|
54
|
+
phase-gate validator 本体(`PhaseStructure` / `EvidenceBundleAssembler` / `Artifact`)には `paths.*` が一切流入していない。同一 config に対して validator 経路ごとに挙動が乖離している状態。
|
|
55
|
+
|
|
56
|
+
### 検討した代替案
|
|
57
|
+
|
|
58
|
+
#### (A) `paths.designDocs` / `paths.inceptionDocs` でトップレベルパスを実際に置換する(**採用**)
|
|
59
|
+
|
|
60
|
+
phase-gate validator のパスリテラルを `{designDocsRoot}` / `{inceptionDocsRoot}` プレースホルダ化し、`paths` config の値で展開する。`Artifact` の `docs/` 接頭辞バリデーションはプレースホルダ展開後に実施するか撤廃する。
|
|
61
|
+
|
|
62
|
+
#### (B) ドキュメントを修正し「`paths.*` は storyReflection 専用」と明記する
|
|
63
|
+
|
|
64
|
+
`paths.designDocs` / `paths.inceptionDocs` は phase-gate ノードのパスには影響しないと documentation を改訂し、「ノードのパスを変えたい場合は `phaseDependencies.preset: "custom"` + `gates[]` で個別に上書きする」ことを推奨する。
|
|
65
|
+
|
|
66
|
+
## Decision
|
|
67
|
+
|
|
68
|
+
**(A) を採用する。** phase-gate validator のパス解決にプレースホルダ展開機構を導入し、`paths.designDocs` / `paths.inceptionDocs` 設定を反映させる。
|
|
69
|
+
|
|
70
|
+
### 採用理由
|
|
71
|
+
|
|
72
|
+
1. **ドキュメントが利用者の合理的期待を形成済み** — `docs/guide/configuration.md:471` の記述により「`paths` を変えれば全 validator / hook が解決する」ことが約束されている。実装をドキュメントに合わせる方が修正の方向性として正しい
|
|
73
|
+
2. **(B) は現状コードでは動作しない** — `Artifact.create()` に `docs/` 接頭辞バリデーション (artifact.ts:48) が存在するため、`gates[]` で `mydocs/...` を指定しても `InvalidArtifactPathError` で拒否される。(B) を採るには結局接頭辞バリデーションの緩和が必要で、(A) と実装コストの大半が重複する
|
|
74
|
+
3. **UX 上の持続可能性** — 28 スキル × 多数の path フィールドを利用者が `gates[]` で個別に書き直す UX は持続不可能。デフォルトを変えるという軽量な操作で済む (A) の方が retrofit-adoption(既存 PJ への phasegate 導入)ユースケースに適合する
|
|
75
|
+
4. **既存の部分対応箇所との整合** — `harness-api` / `agent-integration` 配下の adapter 群は既に `paths.designDocs` を読んでいる。phase-gate validator だけが取り残されている形であり、(A) は不整合を解消する
|
|
76
|
+
|
|
77
|
+
### 新プレースホルダ仕様
|
|
78
|
+
|
|
79
|
+
| プレースホルダ | 解決元 | デフォルト | 既存挙動との関係 |
|
|
80
|
+
|----------------|--------|------------|---|
|
|
81
|
+
| `{designDocsRoot}` | `paths.designDocs` | `docs/product/construction` | 新設 |
|
|
82
|
+
| `{inceptionDocsRoot}` | `paths.inceptionDocs` | `docs/inception` | 新設 |
|
|
83
|
+
| `{unit}` | `scope.unitId` | (変更なし) | 既存 |
|
|
84
|
+
| `{storyId}` | `scope.storyId` | (変更なし) | 既存 |
|
|
85
|
+
|
|
86
|
+
### `Artifact` バリデーションの取り扱い
|
|
87
|
+
|
|
88
|
+
`docs/` 接頭辞バリデーション (artifact.ts:48) は **撤廃する**。理由:
|
|
89
|
+
|
|
90
|
+
- `{designDocsRoot}` / `{inceptionDocsRoot}` のデフォルト値が `docs/...` なので、デフォルト挙動では引き続き `docs/` 配下に展開される
|
|
91
|
+
- 利用者が意図的に `mydocs/...` を選択した場合、それを拒否する正当性がない(`paths` 設定の存在意義そのもの)
|
|
92
|
+
- プレースホルダ展開後の文字列を再度 `docs/` 接頭辞で検査すると、`paths.*` の独自値が `Artifact` 生成時に未展開のまま検査される時系列問題が発生するため、**展開後の検査も行わない**
|
|
93
|
+
- 代替の防御として、`paths.designDocs` / `paths.inceptionDocs` 自体の妥当性は config-foundation の schema validation(既存)で担保する
|
|
94
|
+
|
|
95
|
+
### `EvidenceBundleAssembler` の paths 流入経路
|
|
96
|
+
|
|
97
|
+
`PhaseConfigProviderPort` に新メソッド `getPathRoots()` を追加し、`paths.designDocs` / `paths.inceptionDocs` を取得する。`EvidenceBundleAssembler.assembleForLevel()` 内で paths を取得し、`scope` に追加するか別引数で `Artifact.resolve()` に渡す(具体形は logical-designer フェーズで確定)。
|
|
98
|
+
|
|
99
|
+
### 後方互換戦略
|
|
100
|
+
|
|
101
|
+
- `paths` 未指定または既定値(`docs/product/construction` / `docs/inception`)の場合、要求パスは v0.115.0 以前と完全に同一
|
|
102
|
+
- 既存の `gates[]` でパスを上書きしているユーザーは、上書き値がそのまま使われる挙動を維持する(プレースホルダを含まないリテラルパスはそのまま resolve される)
|
|
103
|
+
- `Artifact` の `docs/` 接頭辞バリデーション撤廃は **緩和方向の変更**であり、既存の正常な config を破壊しない
|
|
104
|
+
|
|
105
|
+
## Consequences
|
|
106
|
+
|
|
107
|
+
### ポジティブ
|
|
108
|
+
|
|
109
|
+
- ドキュメント `configuration.md:471` の記述と実装が一致し、利用者の合理的期待が満たされる
|
|
110
|
+
- `paths.designDocs` / `paths.inceptionDocs` が validator 経路(L2-001 phase-gate, harness-api scanner, agent-integration hook)で一貫して解釈される
|
|
111
|
+
- retrofit-adoption(ISSUE-007)で既存 PJ がドキュメント配置規約を保ったまま phasegate を被せられるようになり、適用範囲が拡大
|
|
112
|
+
- 28 スキル × 多数の path フィールドに `gates[]` で介入させる UX 負担が解消される
|
|
113
|
+
|
|
114
|
+
### ネガティブ / トレードオフ
|
|
115
|
+
|
|
116
|
+
- **ドメイン契約変更**: `Artifact.resolve()` のシグネチャが拡張されるため、phase-dependency-model の domain 層に破壊的変更が入る
|
|
117
|
+
- **緩和策**: 内部 API のみであり、phasegate-cli 利用者には影響しない。テストで後方互換を assert する
|
|
118
|
+
- **`docs/` 接頭辞バリデーション撤廃の緩み**: 任意の文字列パスを許容することで、typo 等の誤設定を `Artifact.create()` 段階で検出できなくなる
|
|
119
|
+
- **緩和策**: `paths.designDocs` / `paths.inceptionDocs` の妥当性は config-foundation の schema validation で検査済み。プレースホルダを含まないリテラル path(`gates[]` 経由)の typo は実行時の `artifactExistenceChecker` で「ファイルが存在しない」として検出される
|
|
120
|
+
- **新プレースホルダの命名選定**: `{designDocsRoot}` / `{inceptionDocsRoot}` は `paths` config キー名と微妙に異なる(`designDocs` vs `designDocsRoot`)
|
|
121
|
+
- **判断**: 既存プレースホルダ `{unit}` / `{storyId}` も config キー名そのままではないため整合的。`Root` suffix で「ルートディレクトリ」であることを明示する方が利用者にとって意味が明確
|
|
122
|
+
|
|
123
|
+
### スコープ外(本 ADR で扱わない)
|
|
124
|
+
|
|
125
|
+
- L2 以外の validator(L1 / L3 / L4)への paths 設定の網羅的反映 — 既に各 validator が個別に `paths.*` を読んでいる箇所があるため、本 ADR は L2-001 phase-gate に閉じる
|
|
126
|
+
- `paths.designDocs` 直下の Unit ディレクトリ命名規約変更
|
|
127
|
+
- skill ファイル(`.claude/skills/`)内の path 参照の動的化
|
|
128
|
+
- `Artifact.path` のユーザー任意プレースホルダ拡張(`{customRoot}` 等)— 本 ADR で導入する 2 個のプレースホルダで現状のニーズは満たされる
|
|
129
|
+
|
|
130
|
+
## Migration
|
|
131
|
+
|
|
132
|
+
1. **ドメイン層**: `Artifact` の `ALLOWED_PLACEHOLDERS` に `designDocsRoot` / `inceptionDocsRoot` を追加し、`docs/` 接頭辞バリデーションを撤廃。`Artifact.resolve()` シグネチャに path roots を渡す経路を追加
|
|
133
|
+
2. **ドメイン定義**: `STANDARD_PHASE_NODES` / `FULL_PHASE_NODES` / `MINIMAL_PHASE_NODES` の path リテラルを `{inceptionDocsRoot}/...` / `{designDocsRoot}/...` に書き換え
|
|
134
|
+
3. **アプリケーション層**: `PhaseConfigProviderPort` に `getPathRoots()` を追加。`EvidenceBundleAssembler` 等で paths を取得して `Artifact.resolve()` に渡す
|
|
135
|
+
4. **インフラ層**: `PhaseConfigProviderPort` 実装で `paths.designDocs` / `paths.inceptionDocs` を `getPathRoots()` から返すように接続
|
|
136
|
+
5. **テスト**:
|
|
137
|
+
- default paths で従来通り `docs/inception/...` を要求すること
|
|
138
|
+
- `paths.inceptionDocs: "mydocs/inception"` で `mydocs/inception/...` を要求すること
|
|
139
|
+
- `Artifact` が `docs/` 以外で始まるパス(プレースホルダ展開後)を許容すること
|
|
140
|
+
6. **ドキュメント**: `docs/guide/configuration.md` に新プレースホルダの動作を明記
|
|
141
|
+
7. **CHANGELOG**: bug fix として記載(GitHub Issue [#1](https://github.com/junpei-9898/phasegate/issues/1) への参照付き)
|
|
142
|
+
|
|
143
|
+
詳細な設計は WI-085 Phase 2(`logical-designer`)で `docs/product/construction/phase-dependency-model/logical_design.md` 等に記録する。
|
|
144
|
+
|
|
145
|
+
## 関連
|
|
146
|
+
|
|
147
|
+
- **WI-085** — 本 ADR を駆動する Work Item。GitHub Issue [#1](https://github.com/junpei-9898/phasegate/issues/1) に対応
|
|
148
|
+
- **`scripts/harness/phase-dependency-model/domain/values/artifact.ts:6-80`** — プレースホルダ仕様 + `docs/` 接頭辞バリデーション、本 ADR の改修対象
|
|
149
|
+
- **`scripts/harness/phase-dependency-model/domain/definitions/standard-phase-nodes.ts:29-`** — ハードコード path(`full-phase-nodes.ts` / `minimal-phase-nodes.ts` も同様)
|
|
150
|
+
- **`scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts:42-49`** — paths 流入欠落点、本 ADR の改修対象
|
|
151
|
+
- **`scripts/harness/phase-dependency-model/domain/ports/phase-config-provider-port.ts`** — `getPathRoots()` 追加対象
|
|
152
|
+
- **`scripts/harness/harness-api/infrastructure/adapters/file-system-artifact-scanner-adapter.ts:118`** — 既に paths を読んでいる先行箇所、整合確認用
|
|
153
|
+
- **`scripts/harness/agent-integration/infrastructure/adapters/phase-gate-query-adapter.ts:60`** — 同上
|
|
154
|
+
- **`docs/guide/configuration.md:445-471`** — 利用者の期待を形成しているドキュメント記述
|
|
155
|
+
- **ADR-007** — harness-config を Single Source of Truth とする決定。本 ADR は ADR-007 の精神(config から validator を駆動する)を phase-gate validator に拡張するもの
|
|
@@ -469,3 +469,26 @@ docs/
|
|
|
469
469
|
```
|
|
470
470
|
|
|
471
471
|
If you move your design documents to a non-default location, update `paths` accordingly so that all validators and hooks resolve files correctly.
|
|
472
|
+
|
|
473
|
+
**How paths flow into the L2 phase-gate validator (since v0.117.0 / WI-085):**
|
|
474
|
+
|
|
475
|
+
`STANDARD_PHASE_NODES` / `FULL_PHASE_NODES` / `MINIMAL_PHASE_NODES` express artifact locations using two placeholders, expanded at validation time from `paths`:
|
|
476
|
+
|
|
477
|
+
| Placeholder | Resolved from | Default |
|
|
478
|
+
|---|---|---|
|
|
479
|
+
| `{designDocsRoot}` | `paths.designDocs` | `docs/product/construction` |
|
|
480
|
+
| `{inceptionDocsRoot}` | `paths.inceptionDocs` | `docs/inception` |
|
|
481
|
+
|
|
482
|
+
Setting `paths.designDocs` to `mydocs/product/construction` makes the L2 phase-gate require `mydocs/product/construction/{unit}/domain_model.md` instead of the default. Default values match the v0.115.0 layout for full backward compatibility.
|
|
483
|
+
|
|
484
|
+
**Out of scope (paths handled by literal references):**
|
|
485
|
+
|
|
486
|
+
A handful of Level 1 product-wide artifacts live at `docs/product/` directly (not under `construction/`) and remain literal:
|
|
487
|
+
|
|
488
|
+
- `docs/product/product_overview.md`
|
|
489
|
+
- `docs/product/user_stories.md`
|
|
490
|
+
- `docs/product/user_story_mapping.md`
|
|
491
|
+
- `docs/product/units/{unit}_unit.md`
|
|
492
|
+
- `docs/product/units/integration_contract.md`
|
|
493
|
+
|
|
494
|
+
These are not controlled by `paths.designDocs` (which targets the *construction* subtree). If your project moves the entire `docs/product/` tree, override these via `phaseDependencies.preset: "custom"` + `gates[]`.
|
package/package.json
CHANGED
package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { ArtifactExistenceCheckerPort } from '../../domain/ports/artifact-existence-checker-port.js';
|
|
7
7
|
import type { PhaseConfigProviderPort } from '../../domain/ports/phase-config-provider-port.js';
|
|
8
8
|
import type { PlanDocumentReaderPort } from '../../domain/ports/plan-document-reader-port.js';
|
|
9
|
+
import type { PathRoots } from '../../domain/values/artifact.js';
|
|
9
10
|
import { PhaseLevel } from '../../domain/values/phase-level.js';
|
|
10
11
|
import type { PhaseNode } from '../../domain/values/phase-node.js';
|
|
11
12
|
import type { PlanEvidence } from '../../domain/values/plan-evidence.js';
|
|
@@ -21,6 +22,7 @@ export interface EvidenceBundle {
|
|
|
21
22
|
readonly artifactStatuses: ReadonlyMap<string, boolean>;
|
|
22
23
|
readonly planEvidences: ReadonlyMap<string, PlanEvidence>;
|
|
23
24
|
readonly planningMode: PlanningMode;
|
|
25
|
+
readonly pathRoots: PathRoots;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export class EvidenceBundleAssembler {
|
|
@@ -40,13 +42,21 @@ export class EvidenceBundleAssembler {
|
|
|
40
42
|
scope: { unitId?: string; storyId?: string },
|
|
41
43
|
): Promise<EvidenceBundle> {
|
|
42
44
|
const planningMode = await this.phaseConfigProvider.getPlanningMode(scope);
|
|
45
|
+
const pathRoots = await this.phaseConfigProvider.getPathRoots();
|
|
43
46
|
const artifacts = nodes.flatMap((node) => node.artifacts);
|
|
44
|
-
const resolvedStatuses = await this.artifactExistenceChecker.checkAll(
|
|
47
|
+
const resolvedStatuses = await this.artifactExistenceChecker.checkAll(
|
|
48
|
+
artifacts,
|
|
49
|
+
scope,
|
|
50
|
+
pathRoots,
|
|
51
|
+
);
|
|
45
52
|
const artifactStatuses = new Map<string, boolean>();
|
|
46
53
|
|
|
47
54
|
for (const artifact of artifacts) {
|
|
48
55
|
try {
|
|
49
|
-
artifactStatuses.set(
|
|
56
|
+
artifactStatuses.set(
|
|
57
|
+
artifact.path,
|
|
58
|
+
resolvedStatuses.get(artifact.resolve(scope, pathRoots)) ?? false,
|
|
59
|
+
);
|
|
50
60
|
} catch {
|
|
51
61
|
artifactStatuses.set(artifact.path, false);
|
|
52
62
|
}
|
|
@@ -66,6 +76,7 @@ export class EvidenceBundleAssembler {
|
|
|
66
76
|
artifactStatuses,
|
|
67
77
|
planEvidences,
|
|
68
78
|
planningMode,
|
|
79
|
+
pathRoots,
|
|
69
80
|
};
|
|
70
81
|
}
|
|
71
82
|
}
|
|
@@ -7,6 +7,7 @@ import type { PhaseInfoDto } from '../dto/phase-info-dto.js';
|
|
|
7
7
|
import type { PhaseDependency } from '../../domain/values/phase-dependency.js';
|
|
8
8
|
import type { PhaseNode } from '../../domain/values/phase-node.js';
|
|
9
9
|
import type { PlanEvidence } from '../../domain/values/plan-evidence.js';
|
|
10
|
+
import { DEFAULT_PATH_ROOTS, type PathRoots } from '../../domain/values/artifact.js';
|
|
10
11
|
|
|
11
12
|
export interface ResolvedPhaseInfo
|
|
12
13
|
extends Omit<PhaseInfoDto, 'unitId' | 'storyId'> {}
|
|
@@ -41,6 +42,7 @@ export class PhaseInfoResolver {
|
|
|
41
42
|
},
|
|
42
43
|
artifactStatuses: ReadonlyMap<string, boolean>,
|
|
43
44
|
planEvidences: ReadonlyMap<string, PlanEvidence>,
|
|
45
|
+
pathRoots: PathRoots = DEFAULT_PATH_ROOTS,
|
|
44
46
|
): ResolvedPhaseInfo {
|
|
45
47
|
const incomingDependencies = collectIncomingDependencies(graph.dependencies);
|
|
46
48
|
const completedNodeKeys = new Set<string>();
|
|
@@ -108,7 +110,7 @@ export class PhaseInfoResolver {
|
|
|
108
110
|
|
|
109
111
|
for (const artifact of node.requiredArtifacts()) {
|
|
110
112
|
if (artifactStatuses.get(artifact.path) !== true) {
|
|
111
|
-
blockers.push(`成果物が不足しています: ${artifact.
|
|
113
|
+
blockers.push(`成果物が不足しています: ${artifact.expandRoots(pathRoots)}`);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -26,7 +26,7 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
26
26
|
createNode(1, 'product-architect', [
|
|
27
27
|
{
|
|
28
28
|
name: 'product-overview-plan',
|
|
29
|
-
path: '
|
|
29
|
+
path: '{inceptionDocsRoot}/_shared/product_overview_plan.md',
|
|
30
30
|
required: true,
|
|
31
31
|
},
|
|
32
32
|
{
|
|
@@ -38,7 +38,7 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
38
38
|
createNode(1, 'story-writer', [
|
|
39
39
|
{
|
|
40
40
|
name: 'story-writer-plan',
|
|
41
|
-
path: '
|
|
41
|
+
path: '{inceptionDocsRoot}/_shared/story_writer_plan.md',
|
|
42
42
|
required: true,
|
|
43
43
|
},
|
|
44
44
|
{
|
|
@@ -50,7 +50,7 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
50
50
|
createNode(1, 'story-mapper', [
|
|
51
51
|
{
|
|
52
52
|
name: 'story-mapping-plan',
|
|
53
|
-
path: '
|
|
53
|
+
path: '{inceptionDocsRoot}/_shared/story_mapping_plan.md',
|
|
54
54
|
required: true,
|
|
55
55
|
},
|
|
56
56
|
{
|
|
@@ -62,7 +62,7 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
62
62
|
createNode(1, 'unit-designer', [
|
|
63
63
|
{
|
|
64
64
|
name: 'unit-design-plan',
|
|
65
|
-
path: '
|
|
65
|
+
path: '{inceptionDocsRoot}/_shared/unit_design_plan.md',
|
|
66
66
|
required: true,
|
|
67
67
|
},
|
|
68
68
|
{
|
|
@@ -79,108 +79,108 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
79
79
|
createNode(2, 'domain-designer', [
|
|
80
80
|
{
|
|
81
81
|
name: 'domain-model-plan',
|
|
82
|
-
path: '
|
|
82
|
+
path: '{inceptionDocsRoot}/{unit}/domain_model_plan.md',
|
|
83
83
|
required: true,
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
86
|
name: 'domain-model',
|
|
87
|
-
path: '
|
|
87
|
+
path: '{designDocsRoot}/{unit}/domain_model.md',
|
|
88
88
|
required: true,
|
|
89
89
|
},
|
|
90
90
|
]),
|
|
91
91
|
createNode(2, 'logical-designer', [
|
|
92
92
|
{
|
|
93
93
|
name: 'logical-design-plan',
|
|
94
|
-
path: '
|
|
94
|
+
path: '{inceptionDocsRoot}/{unit}/logical_design_plan.md',
|
|
95
95
|
required: true,
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
name: 'logical-design',
|
|
99
|
-
path: '
|
|
99
|
+
path: '{designDocsRoot}/{unit}/logical_design.md',
|
|
100
100
|
required: true,
|
|
101
101
|
},
|
|
102
102
|
]),
|
|
103
103
|
createNode(2, 'it-test-designer', [
|
|
104
104
|
{
|
|
105
105
|
name: 'it-test-design-plan',
|
|
106
|
-
path: '
|
|
106
|
+
path: '{inceptionDocsRoot}/{unit}/it_test_design_plan.md',
|
|
107
107
|
required: true,
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
name: 'it-test-design',
|
|
111
|
-
path: '
|
|
111
|
+
path: '{designDocsRoot}/{unit}/it_test_design.md',
|
|
112
112
|
required: true,
|
|
113
113
|
},
|
|
114
114
|
]),
|
|
115
115
|
createNode(2, 'unit-test-designer', [
|
|
116
116
|
{
|
|
117
117
|
name: 'unit-test-design-plan',
|
|
118
|
-
path: '
|
|
118
|
+
path: '{inceptionDocsRoot}/{unit}/unit_test_design_plan.md',
|
|
119
119
|
required: true,
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
name: 'unit-test-design',
|
|
123
|
-
path: '
|
|
123
|
+
path: '{designDocsRoot}/{unit}/unit_test_design.md',
|
|
124
124
|
required: true,
|
|
125
125
|
},
|
|
126
126
|
]),
|
|
127
127
|
createNode(2, 'it-test-logic-designer', [
|
|
128
128
|
{
|
|
129
129
|
name: 'it-test-logic-plan',
|
|
130
|
-
path: '
|
|
130
|
+
path: '{inceptionDocsRoot}/{unit}/it_test_logic_plan.md',
|
|
131
131
|
required: true,
|
|
132
132
|
},
|
|
133
133
|
{
|
|
134
134
|
name: 'it-test-logic',
|
|
135
|
-
path: '
|
|
135
|
+
path: '{designDocsRoot}/{unit}/it_test_logic.md',
|
|
136
136
|
required: true,
|
|
137
137
|
},
|
|
138
138
|
]),
|
|
139
139
|
createNode(2, 'unit-test-logic-designer', [
|
|
140
140
|
{
|
|
141
141
|
name: 'unit-test-logic-plan',
|
|
142
|
-
path: '
|
|
142
|
+
path: '{inceptionDocsRoot}/{unit}/unit_test_logic_plan.md',
|
|
143
143
|
required: true,
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
name: 'unit-test-logic',
|
|
147
|
-
path: '
|
|
147
|
+
path: '{designDocsRoot}/{unit}/unit_test_logic.md',
|
|
148
148
|
required: true,
|
|
149
149
|
},
|
|
150
150
|
]),
|
|
151
151
|
createNode(3, 'logical-designer', [
|
|
152
152
|
{
|
|
153
153
|
name: 'story-logical-design-plan',
|
|
154
|
-
path: '
|
|
154
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/logical_design_plan.md',
|
|
155
155
|
required: false,
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
name: 'story-logical-design',
|
|
159
|
-
path: '
|
|
159
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/logical_design.md',
|
|
160
160
|
required: false,
|
|
161
161
|
},
|
|
162
162
|
]),
|
|
163
163
|
createNode(3, 'scenario-test-designer', [
|
|
164
164
|
{
|
|
165
165
|
name: 'scenario-test-plan',
|
|
166
|
-
path: '
|
|
166
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/scenario_test_plan.md',
|
|
167
167
|
required: false,
|
|
168
168
|
},
|
|
169
169
|
{
|
|
170
170
|
name: 'scenario-test-design',
|
|
171
|
-
path: '
|
|
171
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/scenario_test_design.md',
|
|
172
172
|
required: false,
|
|
173
173
|
},
|
|
174
174
|
]),
|
|
175
175
|
createNode(3, 'scenario-test-logic-designer', [
|
|
176
176
|
{
|
|
177
177
|
name: 'scenario-test-logic-plan',
|
|
178
|
-
path: '
|
|
178
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/scenario_test_logic_plan.md',
|
|
179
179
|
required: false,
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
name: 'scenario-test-logic',
|
|
183
|
-
path: '
|
|
183
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/scenario_test_logic.md',
|
|
184
184
|
required: false,
|
|
185
185
|
},
|
|
186
186
|
]),
|
|
@@ -188,7 +188,7 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
188
188
|
createNode(3, 'story-implementor', [
|
|
189
189
|
{
|
|
190
190
|
name: 'tdd-implementation-plan',
|
|
191
|
-
path: '
|
|
191
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/tdd_implementation_plan.md',
|
|
192
192
|
required: false,
|
|
193
193
|
},
|
|
194
194
|
]),
|
|
@@ -26,7 +26,7 @@ export const MINIMAL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
26
26
|
createNode(1, 'product-architect', [
|
|
27
27
|
{
|
|
28
28
|
name: 'product-overview-plan',
|
|
29
|
-
path: '
|
|
29
|
+
path: '{inceptionDocsRoot}/_shared/product_overview_plan.md',
|
|
30
30
|
required: true,
|
|
31
31
|
},
|
|
32
32
|
{
|
|
@@ -38,12 +38,12 @@ export const MINIMAL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
38
38
|
createNode(2, 'logical-designer', [
|
|
39
39
|
{
|
|
40
40
|
name: 'logical-design-plan',
|
|
41
|
-
path: '
|
|
41
|
+
path: '{inceptionDocsRoot}/{unit}/logical_design_plan.md',
|
|
42
42
|
required: true,
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
name: 'logical-design',
|
|
46
|
-
path: '
|
|
46
|
+
path: '{designDocsRoot}/{unit}/logical_design.md',
|
|
47
47
|
required: true,
|
|
48
48
|
},
|
|
49
49
|
]),
|
|
@@ -26,7 +26,7 @@ export const STANDARD_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
26
26
|
createNode(1, 'product-architect', [
|
|
27
27
|
{
|
|
28
28
|
name: 'product-overview-plan',
|
|
29
|
-
path: '
|
|
29
|
+
path: '{inceptionDocsRoot}/_shared/product_overview_plan.md',
|
|
30
30
|
required: true,
|
|
31
31
|
},
|
|
32
32
|
{
|
|
@@ -38,7 +38,7 @@ export const STANDARD_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
38
38
|
createNode(1, 'story-writer', [
|
|
39
39
|
{
|
|
40
40
|
name: 'story-writer-plan',
|
|
41
|
-
path: '
|
|
41
|
+
path: '{inceptionDocsRoot}/_shared/story_writer_plan.md',
|
|
42
42
|
required: true,
|
|
43
43
|
},
|
|
44
44
|
{
|
|
@@ -50,60 +50,60 @@ export const STANDARD_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
50
50
|
createNode(2, 'domain-designer', [
|
|
51
51
|
{
|
|
52
52
|
name: 'domain-model-plan',
|
|
53
|
-
path: '
|
|
53
|
+
path: '{inceptionDocsRoot}/{unit}/domain_model_plan.md',
|
|
54
54
|
required: true,
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
name: 'domain-model',
|
|
58
|
-
path: '
|
|
58
|
+
path: '{designDocsRoot}/{unit}/domain_model.md',
|
|
59
59
|
required: true,
|
|
60
60
|
},
|
|
61
61
|
]),
|
|
62
62
|
createNode(2, 'logical-designer', [
|
|
63
63
|
{
|
|
64
64
|
name: 'logical-design-plan',
|
|
65
|
-
path: '
|
|
65
|
+
path: '{inceptionDocsRoot}/{unit}/logical_design_plan.md',
|
|
66
66
|
required: true,
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
name: 'logical-design',
|
|
70
|
-
path: '
|
|
70
|
+
path: '{designDocsRoot}/{unit}/logical_design.md',
|
|
71
71
|
required: true,
|
|
72
72
|
},
|
|
73
73
|
]),
|
|
74
74
|
createNode(3, 'logical-designer', [
|
|
75
75
|
{
|
|
76
76
|
name: 'story-logical-design-plan',
|
|
77
|
-
path: '
|
|
77
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/logical_design_plan.md',
|
|
78
78
|
required: false,
|
|
79
79
|
},
|
|
80
80
|
{
|
|
81
81
|
name: 'story-logical-design',
|
|
82
|
-
path: '
|
|
82
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/logical_design.md',
|
|
83
83
|
required: false,
|
|
84
84
|
},
|
|
85
85
|
]),
|
|
86
86
|
createNode(3, 'scenario-test-designer', [
|
|
87
87
|
{
|
|
88
88
|
name: 'scenario-test-plan',
|
|
89
|
-
path: '
|
|
89
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/scenario_test_plan.md',
|
|
90
90
|
required: false,
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
name: 'scenario-test-design',
|
|
94
|
-
path: '
|
|
94
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/scenario_test_design.md',
|
|
95
95
|
required: false,
|
|
96
96
|
},
|
|
97
97
|
]),
|
|
98
98
|
createNode(3, 'scenario-test-logic-designer', [
|
|
99
99
|
{
|
|
100
100
|
name: 'scenario-test-logic-plan',
|
|
101
|
-
path: '
|
|
101
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/scenario_test_logic_plan.md',
|
|
102
102
|
required: false,
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
name: 'scenario-test-logic',
|
|
106
|
-
path: '
|
|
106
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/scenario_test_logic.md',
|
|
107
107
|
required: false,
|
|
108
108
|
},
|
|
109
109
|
]),
|
|
@@ -111,7 +111,7 @@ export const STANDARD_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
111
111
|
createNode(3, 'story-implementor', [
|
|
112
112
|
{
|
|
113
113
|
name: 'tdd-implementation-plan',
|
|
114
|
-
path: '
|
|
114
|
+
path: '{inceptionDocsRoot}/{unit}/{storyId}/tdd_implementation_plan.md',
|
|
115
115
|
required: false,
|
|
116
116
|
},
|
|
117
117
|
]),
|
|
@@ -10,7 +10,7 @@ import { MINIMAL_PHASE_NODES } from '../definitions/minimal-phase-nodes.js';
|
|
|
10
10
|
import { STANDARD_PHASE_DEPENDENCIES } from '../definitions/standard-phase-dependencies.js';
|
|
11
11
|
import { STANDARD_PHASE_NODES } from '../definitions/standard-phase-nodes.js';
|
|
12
12
|
import { GateGraph } from '../services/gate-graph.js';
|
|
13
|
-
import { Artifact } from '../values/artifact.js';
|
|
13
|
+
import { Artifact, DEFAULT_PATH_ROOTS, type PathRoots } from '../values/artifact.js';
|
|
14
14
|
import { CustomRule, InvalidCustomRuleError } from '../values/custom-rule.js';
|
|
15
15
|
import { GateDefinition } from '../values/gate-definition.js';
|
|
16
16
|
import { PhaseCustomizationPolicy } from '../values/phase-customization-policy.js';
|
|
@@ -76,13 +76,14 @@ const buildNodeIndex = (nodes: readonly PhaseNode[]): ReadonlyMap<string, PhaseN
|
|
|
76
76
|
const collectMissingArtifactBlockers = (
|
|
77
77
|
nodes: readonly PhaseNode[],
|
|
78
78
|
artifactStatuses: ReadonlyMap<string, boolean>,
|
|
79
|
+
pathRoots: PathRoots,
|
|
79
80
|
): readonly string[] => {
|
|
80
81
|
const blockers: string[] = [];
|
|
81
82
|
|
|
82
83
|
for (const node of nodes) {
|
|
83
84
|
for (const artifact of node.requiredArtifacts()) {
|
|
84
85
|
if (!artifactStatuses.get(artifact.path)) {
|
|
85
|
-
blockers.push(`成果物が不足しています: ${artifact.
|
|
86
|
+
blockers.push(`成果物が不足しています: ${artifact.expandRoots(pathRoots)}`);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -220,6 +221,7 @@ export class PhaseStructure {
|
|
|
220
221
|
artifactStatuses: ReadonlyMap<string, boolean>;
|
|
221
222
|
planEvidences: ReadonlyMap<string, PlanEvidence>;
|
|
222
223
|
planningMode: PlanningMode;
|
|
224
|
+
pathRoots?: PathRoots;
|
|
223
225
|
},
|
|
224
226
|
scope?: {
|
|
225
227
|
unitId?: string;
|
|
@@ -237,13 +239,14 @@ export class PhaseStructure {
|
|
|
237
239
|
});
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
const pathRoots = evidence.pathRoots ?? DEFAULT_PATH_ROOTS;
|
|
240
243
|
const prerequisiteNodes = [1, 2, 3]
|
|
241
244
|
.map((value) => PhaseLevel.create(value))
|
|
242
245
|
.filter((level) => level.isPrerequisiteOf(targetLevel))
|
|
243
246
|
.flatMap((level) => this.getPhaseNodes(level));
|
|
244
247
|
|
|
245
248
|
const blockers = [
|
|
246
|
-
...collectMissingArtifactBlockers(prerequisiteNodes, evidence.artifactStatuses),
|
|
249
|
+
...collectMissingArtifactBlockers(prerequisiteNodes, evidence.artifactStatuses, pathRoots),
|
|
247
250
|
];
|
|
248
251
|
const completedNodeKeys = new Set<string>();
|
|
249
252
|
const warnings: string[] = [];
|
|
@@ -289,7 +292,7 @@ export class PhaseStructure {
|
|
|
289
292
|
let nodeComplete = true;
|
|
290
293
|
for (const artifact of node.artifacts) {
|
|
291
294
|
// Evidence map may be keyed by unresolved path (artifact.path) or resolved path
|
|
292
|
-
const resolvedPath = artifact.resolve(scope);
|
|
295
|
+
const resolvedPath = artifact.resolve(scope, pathRoots);
|
|
293
296
|
const exists = evidence.artifactStatuses.get(artifact.path)
|
|
294
297
|
?? evidence.artifactStatuses.get(resolvedPath)
|
|
295
298
|
?? false;
|
package/scripts/harness/phase-dependency-model/domain/ports/artifact-existence-checker-port.ts
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* @unit phase-dependency-model
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Artifact } from '../values/artifact.js';
|
|
6
|
+
import type { Artifact, PathRoots } from '../values/artifact.js';
|
|
7
7
|
|
|
8
8
|
export interface ArtifactExistenceCheckerPort {
|
|
9
9
|
checkAll(
|
|
10
10
|
artifacts: readonly Artifact[],
|
|
11
11
|
scope: { unitId?: string; storyId?: string },
|
|
12
|
+
pathRoots?: PathRoots,
|
|
12
13
|
): Promise<ReadonlyMap<string, boolean>>;
|
|
13
14
|
}
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
import type { PhaseCustomizationPolicy } from '../values/phase-customization-policy.js';
|
|
7
7
|
import type { PlanningMode } from '../values/planning-mode.js';
|
|
8
8
|
import type { StoryReflectionConfig } from '../values/story-reflection-config.js';
|
|
9
|
+
import type { PathRoots } from '../values/artifact.js';
|
|
9
10
|
|
|
10
11
|
export interface PhaseConfigProviderPort {
|
|
11
12
|
getPlanningMode(scope: { unitId?: string; storyId?: string }): Promise<PlanningMode>;
|
|
12
13
|
getCustomizationPolicy(): Promise<PhaseCustomizationPolicy>;
|
|
13
14
|
getReportingOutputDir(): Promise<string>;
|
|
14
15
|
getStoryReflectionConfig(): Promise<StoryReflectionConfig>;
|
|
16
|
+
getPathRoots(): Promise<PathRoots>;
|
|
15
17
|
}
|
|
@@ -3,10 +3,25 @@
|
|
|
3
3
|
* @unit phase-dependency-model
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const ALLOWED_PLACEHOLDERS = new Set([
|
|
6
|
+
const ALLOWED_PLACEHOLDERS = new Set([
|
|
7
|
+
'unit',
|
|
8
|
+
'storyId',
|
|
9
|
+
'designDocsRoot',
|
|
10
|
+
'inceptionDocsRoot',
|
|
11
|
+
]);
|
|
7
12
|
const PLACEHOLDER_PATTERN = /\{([^}]+)\}/g;
|
|
8
13
|
const UNRESOLVED_PLACEHOLDER_PATTERN = /\{[^}]+\}/;
|
|
9
14
|
|
|
15
|
+
export interface PathRoots {
|
|
16
|
+
readonly designDocsRoot: string;
|
|
17
|
+
readonly inceptionDocsRoot: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_PATH_ROOTS: PathRoots = Object.freeze({
|
|
21
|
+
designDocsRoot: 'docs/product/construction',
|
|
22
|
+
inceptionDocsRoot: 'docs/inception',
|
|
23
|
+
});
|
|
24
|
+
|
|
10
25
|
export interface ArtifactCreateArgs {
|
|
11
26
|
readonly name: string;
|
|
12
27
|
readonly path: string;
|
|
@@ -45,7 +60,7 @@ export class Artifact {
|
|
|
45
60
|
static create(args: ArtifactCreateArgs): Artifact {
|
|
46
61
|
const normalizedPath = args.path.trim();
|
|
47
62
|
|
|
48
|
-
if (normalizedPath.length === 0
|
|
63
|
+
if (normalizedPath.length === 0) {
|
|
49
64
|
throw new InvalidArtifactPathError(args.path);
|
|
50
65
|
}
|
|
51
66
|
|
|
@@ -66,11 +81,25 @@ export class Artifact {
|
|
|
66
81
|
return this.path.endsWith('_plan.md');
|
|
67
82
|
}
|
|
68
83
|
|
|
69
|
-
resolve(
|
|
84
|
+
resolve(
|
|
85
|
+
scope: { unitId?: string; storyId?: string },
|
|
86
|
+
pathRoots: PathRoots = DEFAULT_PATH_ROOTS,
|
|
87
|
+
): string {
|
|
70
88
|
let resolvedPath = this.path;
|
|
71
89
|
|
|
90
|
+
resolvedPath = resolvedPath.replaceAll(
|
|
91
|
+
'{designDocsRoot}',
|
|
92
|
+
pathRoots.designDocsRoot,
|
|
93
|
+
);
|
|
94
|
+
resolvedPath = resolvedPath.replaceAll(
|
|
95
|
+
'{inceptionDocsRoot}',
|
|
96
|
+
pathRoots.inceptionDocsRoot,
|
|
97
|
+
);
|
|
72
98
|
resolvedPath = resolvedPath.replaceAll('{unit}', scope.unitId ?? '{unit}');
|
|
73
|
-
resolvedPath = resolvedPath.replaceAll(
|
|
99
|
+
resolvedPath = resolvedPath.replaceAll(
|
|
100
|
+
'{storyId}',
|
|
101
|
+
scope.storyId ?? '{storyId}',
|
|
102
|
+
);
|
|
74
103
|
|
|
75
104
|
if (this.required && UNRESOLVED_PLACEHOLDER_PATTERN.test(resolvedPath)) {
|
|
76
105
|
throw new InvalidArtifactPathError(this.path);
|
|
@@ -79,6 +108,12 @@ export class Artifact {
|
|
|
79
108
|
return resolvedPath;
|
|
80
109
|
}
|
|
81
110
|
|
|
111
|
+
expandRoots(pathRoots: PathRoots = DEFAULT_PATH_ROOTS): string {
|
|
112
|
+
return this.path
|
|
113
|
+
.replaceAll('{designDocsRoot}', pathRoots.designDocsRoot)
|
|
114
|
+
.replaceAll('{inceptionDocsRoot}', pathRoots.inceptionDocsRoot);
|
|
115
|
+
}
|
|
116
|
+
|
|
82
117
|
equals(other: Artifact): boolean {
|
|
83
118
|
return (
|
|
84
119
|
this.name === other.name &&
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { CustomRule } from '../../domain/values/custom-rule.js';
|
|
13
13
|
import { StoryReflectionConfig } from '../../domain/values/story-reflection-config.js';
|
|
14
14
|
import { StoryReflectionMapping } from '../../domain/values/story-reflection-mapping.js';
|
|
15
|
+
import { DEFAULT_PATH_ROOTS, type PathRoots } from '../../domain/values/artifact.js';
|
|
15
16
|
import { FULL_STORY_REFLECTION_DEFAULTS } from '../../domain/definitions/full-story-reflection-defaults.js';
|
|
16
17
|
import { STANDARD_STORY_REFLECTION_DEFAULTS } from '../../domain/definitions/standard-story-reflection-defaults.js';
|
|
17
18
|
import { MINIMAL_STORY_REFLECTION_DEFAULTS } from '../../domain/definitions/minimal-story-reflection-defaults.js';
|
|
@@ -54,8 +55,14 @@ export interface PhaseConfigSection {
|
|
|
54
55
|
};
|
|
55
56
|
readonly storyReflection?: StoryReflectionSectionInput;
|
|
56
57
|
readonly reportingOutputDir?: string;
|
|
58
|
+
readonly paths?: {
|
|
59
|
+
readonly designDocs?: string;
|
|
60
|
+
readonly inceptionDocs?: string;
|
|
61
|
+
};
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
const trimTrailingSlash = (value: string): string => value.replace(/\/+$/, '');
|
|
65
|
+
|
|
59
66
|
/**
|
|
60
67
|
* プリセットごとの storyReflection デフォルト定義。
|
|
61
68
|
* `default` は v1.0 で `full` に統合されたため同一値にエイリアスする。
|
|
@@ -135,6 +142,17 @@ export class HarnessConfigPhaseConfigProvider
|
|
|
135
142
|
return this.config.reportingOutputDir ?? this.defaultOutputDir;
|
|
136
143
|
}
|
|
137
144
|
|
|
145
|
+
async getPathRoots(): Promise<PathRoots> {
|
|
146
|
+
const paths = this.config.paths;
|
|
147
|
+
const designDocsRoot = paths?.designDocs
|
|
148
|
+
? trimTrailingSlash(paths.designDocs)
|
|
149
|
+
: DEFAULT_PATH_ROOTS.designDocsRoot;
|
|
150
|
+
const inceptionDocsRoot = paths?.inceptionDocs
|
|
151
|
+
? trimTrailingSlash(paths.inceptionDocs)
|
|
152
|
+
: DEFAULT_PATH_ROOTS.inceptionDocsRoot;
|
|
153
|
+
return { designDocsRoot, inceptionDocsRoot };
|
|
154
|
+
}
|
|
155
|
+
|
|
138
156
|
async getStoryReflectionConfig(): Promise<StoryReflectionConfig> {
|
|
139
157
|
const policy = await this.getCustomizationPolicy();
|
|
140
158
|
const presetDefault = PRESET_STORY_REFLECTION_DEFAULTS[policy.preset];
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { access } from 'node:fs/promises';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
8
|
import type { ArtifactExistenceCheckerPort } from '../../domain/ports/artifact-existence-checker-port.js';
|
|
9
|
-
import type { Artifact } from '../../domain/values/artifact.js';
|
|
9
|
+
import type { Artifact, PathRoots } from '../../domain/values/artifact.js';
|
|
10
10
|
|
|
11
11
|
export interface FileSystemArtifactExistenceCheckerDeps {
|
|
12
12
|
readonly rootDir: string;
|
|
@@ -24,13 +24,14 @@ export class FileSystemArtifactExistenceChecker
|
|
|
24
24
|
async checkAll(
|
|
25
25
|
artifacts: readonly Artifact[],
|
|
26
26
|
scope: { unitId?: string; storyId?: string },
|
|
27
|
+
pathRoots?: PathRoots,
|
|
27
28
|
): Promise<ReadonlyMap<string, boolean>> {
|
|
28
29
|
const results = new Map<string, boolean>();
|
|
29
30
|
|
|
30
31
|
for (const artifact of artifacts) {
|
|
31
32
|
let resolvedPath: string;
|
|
32
33
|
try {
|
|
33
|
-
resolvedPath = artifact.resolve(scope);
|
|
34
|
+
resolvedPath = artifact.resolve(scope, pathRoots);
|
|
34
35
|
} catch {
|
|
35
36
|
results.set(artifact.path, false);
|
|
36
37
|
continue;
|
package/scripts/harness/skill-quality/infrastructure/adapters/git-commit-executor-adapter.ts
CHANGED
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit skill-quality
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
6
|
import type { CommitExecutorPort } from '../../domain/ports/commit-executor-port.js';
|
|
7
7
|
import type { CommitMessage } from '../../domain/value-objects/commit-message.js';
|
|
8
8
|
import { SkillQualityError } from '../../domain/errors/skill-quality-error.js';
|
|
9
9
|
|
|
10
|
+
// WI-036: 配列引数で execFileSync を直接呼ぶことでシェル経由のメタ文字評価を遮断する。
|
|
11
|
+
type GitExecutor = (file: string, args: readonly string[], options: { stdio: 'pipe' }) => Buffer;
|
|
12
|
+
|
|
10
13
|
export class GitCommitExecutorAdapter implements CommitExecutorPort {
|
|
14
|
+
constructor(private readonly gitExecutor: GitExecutor = execFileSync) {}
|
|
15
|
+
|
|
11
16
|
async commit(commitMessage: CommitMessage): Promise<void> {
|
|
12
17
|
const message = commitMessage.format();
|
|
13
18
|
try {
|
|
14
|
-
|
|
19
|
+
this.gitExecutor('git', ['commit', '-m', message], { stdio: 'pipe' });
|
|
15
20
|
} catch (err) {
|
|
16
21
|
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
17
22
|
// "nothing to commit" を検出して分かりやすいメッセージに変換
|