phasegate 0.130.0 → 0.132.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 (22) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/docs/ADR/ADR-017-warning-severity-aggregation.md +202 -0
  3. package/docs/guide/configuration.md +34 -0
  4. package/docs/guide/layer-model.md +19 -0
  5. package/package.json +1 -1
  6. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +4 -1
  7. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -0
  8. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +7 -0
  9. package/scripts/harness/config-foundation/domain/value-objects/validate-config.ts +32 -0
  10. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +3 -0
  11. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +3 -0
  12. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +3 -0
  13. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +9 -0
  14. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +9 -0
  15. package/scripts/harness/main.ts +16 -1
  16. package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +5 -1
  17. package/scripts/harness/validator-system/composition-root.ts +9 -1
  18. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +2 -1
  19. package/scripts/harness/validator-system/domain/value-objects/dead-code-report.ts +3 -2
  20. package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +2 -1
  21. package/scripts/harness/validator-system/presentation/formatters/human-validation-result-formatter.ts +9 -1
  22. package/scripts/harness/validator-system/presentation/handlers/run-validators-handler.ts +6 -1
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.132.0] - 2026-05-08
11
+
12
+ ### Fixed
13
+
14
+ - **WI-094 follow-up — `DriftReport` / `ConsistencyReport` / `DeadCodeReport` の `toHarnessError(s)` が `severity: 'error'` を hardcode していたため v0.131.0 の集計セマンティクス修正が L4-001/002/003 で実機に反映されなかった (post-publish dogfood で発見)** — `error catalog` の `defaultSeverity: warning` 宣言と各 report の `toHarnessError(s)` 出力 severity が乖離しており、aggregator が `severity !== 'warning'` で hasNonWarningError と判定して overall FAIL となっていた。
15
+ - **修正対象**:
16
+ - `scripts/harness/validator-system/domain/value-objects/drift-report.ts:47` — L4-001 drift error の severity を `'error'` → `'warning'`
17
+ - `scripts/harness/validator-system/domain/value-objects/consistency-report.ts:49` — L4-002 consistency error の severity を `'error'` → `'warning'`
18
+ - `scripts/harness/validator-system/domain/value-objects/dead-code-report.ts:46/54` — L4-003 dead-code error の severity を `'error'` → `'warning'` (unused export / unreachable code 双方)
19
+ - **dogfood 結果 (v0.131.0 の状態)**: `/private/tmp/phasegate-dogfood-wi094` で drift 2 件発生させて `validate --layer L4 --format human` を実行 → `[FAIL] L4-001` / overall FAIL ✗ / exit 1。期待は `[WARN] L4-001` / overall PASS ✓ / exit 0。原因は本 fix で解消。
20
+ - **後方互換**: `validate.failOnWarning: true` を設定している user の挙動は不変 (warning でも overall FAIL)。`failOnWarning: false` (default for `minimal` / `standard`) の user のみ exit code 0 に変わる。これは ADR-017 で承認済みの BREAKING の意図通りで、v0.131.0 で動かなかった部分が初めて動く形。
21
+ - **post-publish dogfood**: WI-094 description.md に v0.132.0 dogfood 結果を反映。
22
+
23
+ ## [0.131.0] - 2026-05-08
24
+
25
+ ### Changed (BREAKING for `standard` / `minimal` preset users)
26
+
27
+ - **WI-094 — warning-severity validator の集計セマンティクスを修正 (ADR-017, GitHub Issue #4 finding #2)** — `error catalog` で `defaultSeverity: warning` と宣言された validator (L4-001 drift / L4-002 consistency / L4-003 dead-code) が fail を返した際、`overallPassed` 判定が severity を見ず常に `failedValidators++` していたバグを修正。warning-only validator fail は `overall PASS / exit 0` を default に変更。
28
+ - **修正前**: `aggregate-validation-results-usecase.ts:35-42` の `hasFail = !result.passed || (failOnWarning && hasWarnings)` で `failOnWarning` flag が常に dead code 化していた。warning-only fail でも `[FAIL]` 表示・exit 1 となり、reporter (nakataj-mti) は plan-level workaround (false positive 注釈) で凌いでいた。
29
+ - **修正後**: `hasFail = !result.passed && (isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings))` で severity を反映。`failOnWarning=true` で旧挙動 (warning も fail) に opt-in 可能。
30
+ - **新 config フラグ**: `phasegate.config.json` に `validate.failOnWarning: boolean` を追加。default は `false`。preset 別 default: `minimal` / `standard` = `false`、`strict` = `true` (ci-governance preset adapter の precedent と整合)。schema v2 / v3 双方で optional property として追加 — 既存 user の config はそのまま valid。
31
+ - **新 CLI flag**: `--fail-on-warning` / `--no-fail-on-warning` の tri-state 化。両方未指定の場合は config 値を使用 (CLI > config 優先順位)。
32
+ - **Formatter 改修**: `human` formatter で `[FAIL]` と `[WARN]` を分離表示。warning-only fail は `[WARN] L4-001` として表示。`agent` / `ci` formatter (JSON) は既存 `severity` field 構造を維持し、後方互換性を確保。
33
+ - **Migration**: warning-only fail で CI を止めたい既存 user は `phasegate.config.json` に以下を追加:
34
+ ```json
35
+ { "validate": { "failOnWarning": true } }
36
+ ```
37
+ または CLI で `--fail-on-warning` を指定。`strict` preset 利用者は default で `failOnWarning: true` のため migration 不要。
38
+ - **回帰テスト**: warning-only / error-only / mixed / failOnWarning=true / 防御的 (passed=false かつ errors=[]) の 5 ケースを `aggregate-validation-results-usecase.test.ts` に追加。`human-validation-result-formatter.test.ts` 新設で `[WARN]` / `[FAIL]` / `[PASS]` / `[SKIP]` の表示を網羅。
39
+ - **検証 (publish 前)**: `pnpm test` 454 files / 3531 tests pass (baseline +11)。`npx phasegate validate --layer L2` は pre-existing fail のみ (L2-001 artifact 不足、本 WI とは無関係)。
40
+ - **post-publish dogfood**: WI-094 description.md / `tdd_implementation_plan.md` に dogfood 結果を反映予定。
41
+
10
42
  ## [0.130.0] - 2026-05-08
11
43
 
12
44
  ### Fixed
@@ -15,7 +47,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
47
  - **phase-dependency-model**: Level 1 product 文書を `{designDocsRoot}/../...` で定義し、`Artifact.resolve()` / `expandRoots()` で POSIX 正規化。`paths.designDocs: "mydocs/product/construction"` の場合、L2 blocker は `mydocs/product/product_overview.md` / `mydocs/product/user_stories.md` を参照する。
16
48
  - **traceability-model**: `MarkdownStoryCatalogGateway` / `MarkdownDesignDocumentGateway` / `MarkdownUnitDefinitionGateway` / `TraceabilityChainBuilder` に custom design docs root を注入し、`mydocs/product/user_stories.md` と `mydocs/product/construction/{unit}` を読むよう修正。
17
49
  - **後方互換**: config 未指定時は従来通り `docs/product/user_stories.md` / `docs/product/construction/{unit}` を利用。
18
- - **検証 (publish 前 local/self-host)**: `pnpm exec tsc --noEmit` pass。対象テスト 4 files / 29 tests pass。`pnpm test` は 453 files / 3520 tests pass。`/private/tmp/phasegate-local-wi093` の symlink なし reporter fixture で `validate --layer L2 --format human` を実行し、L2-001 blocker が `mydocs/product/product_overview.md` を参照することを確認。post-publish dogfood は publish 後に実施予定。
50
+ - **検証 (publish 前 local/self-host)**: `pnpm exec tsc --noEmit` pass。対象テスト 4 files / 29 tests pass。`pnpm test` は 453 files / 3520 tests pass。`/private/tmp/phasegate-local-wi093` の symlink なし reporter fixture で `validate --layer L2 --format human` を実行し、L2-001 blocker が `mydocs/product/product_overview.md` を参照することを確認。
51
+ - **post-publish dogfood**: `/private/tmp/phasegate-dogfood-wi093-published` で `npx phasegate@0.130.0` を使い検証済み。custom `paths.designDocs: "mydocs/product/construction"` では L2-001 blocker が `mydocs/product/product_overview.md` を参照し、default `paths.designDocs: "docs/product/construction"` では `docs/product/product_overview.md` を参照。`validate-metadata mydocs/product/construction/sample/domain_model.md` は `mydocs/product/user_stories.md` 由来で PASS。`docs/product` symlink workaround なし。
19
52
 
20
53
  ## [0.129.0] - 2026-05-08
21
54
 
@@ -0,0 +1,202 @@
1
+ # ADR-017: warning-severity validator の集計セマンティクス
2
+
3
+ ## Status
4
+
5
+ Accepted — 2026-05-08
6
+
7
+ ## Context
8
+
9
+ `phasegate validate` の集計ロジック (`aggregate-validation-results-usecase.ts:67`) は `overallPassed = failedValidators === 0` で総合判定するが、この判定は **error severity を考慮していない**。
10
+
11
+ その結果、`error catalog` 上で `defaultSeverity: warning` と宣言されている validator (L4-001 / L4-002 / L4-003) が fail を報告すると、warning-only にもかかわらず:
12
+
13
+ - 個別 validator: `[FAIL] L4-001`
14
+ - 総合判定: `FAIL ✗`
15
+ - exit code: 非ゼロ (1)
16
+
17
+ となる。GitHub Issue [#4](https://github.com/junpei-9898/phasegate/issues/4) (reporter: nakataj-mti, 2026-05-08) で報告され、reporter は plan-level workaround (「validate FAIL を許容、L4-001 false positive と plan に明記」) で凌いでいる。WI-091 finding #2 → WI-094 として起票。
18
+
19
+ ### バグの実体(grep ベース、2026-05-08)
20
+
21
+ #### 1. 集計ロジックが severity を見ていない
22
+
23
+ `scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts:35-42`:
24
+
25
+ ```ts
26
+ const hasWarnings = result.errors.some((e) => e.severity === 'warning');
27
+ const hasFail = !result.passed || (failOnWarning && hasWarnings);
28
+
29
+ if (hasFail) {
30
+ failedValidators++;
31
+ } else {
32
+ passedValidators++;
33
+ }
34
+ ```
35
+
36
+ `failOnWarning` パラメータは存在するが、`!result.passed` を OR 第一項に置いているため、**warning-only validator が `ValidationResult.fail([warningError])` を返した瞬間に `failOnWarning` の値に関係なく `failedValidators++` が確定する**。`failOnWarning` 経路は実質デッドコード。
37
+
38
+ #### 2. exit code が overallPassed に直結
39
+
40
+ `scripts/harness/validator-system/presentation/handlers/report-validation-results-handler.ts:62`:
41
+
42
+ ```ts
43
+ const exitCode = report.overallPassed ? 0 : 1;
44
+ ```
45
+
46
+ `overallPassed` が severity を見ない以上、warning-only fail は必ず exit 1。
47
+
48
+ #### 3. `--fail-on-warning` CLI flag は通っているが効かない
49
+
50
+ `scripts/harness/main.ts:997`:
51
+
52
+ ```ts
53
+ const failOnWarning = hasFlag(args, "--fail-on-warning");
54
+ ```
55
+
56
+ CLI からは flag が `RunFullValidationInput.failOnWarning` まで伝搬しているが、(1) のバグにより default 挙動 (warning も fail) と `--fail-on-warning` 指定時の挙動が同一になっている。
57
+
58
+ #### 4. config からは設定不可
59
+
60
+ `phasegate.config.json` には `validate.failOnWarning` 相当のキーが存在しない。CLI flag のみ。CI スクリプトを書く側で都度 `--fail-on-warning` を付け外しする必要があり、config single source (ADR-007) の精神に反する。
61
+
62
+ ### 検討した代替案
63
+
64
+ #### (A) warning-only fail は overall PASS / exit 0 を default にする(**採用**)
65
+
66
+ 集計ロジックを修正し、validator が warning-only fail を返した場合は `failedValidators` にカウントしない。`failOnWarning: true` で従来挙動 (warning も fail) に opt-in。`validate.failOnWarning: boolean` を config schema に追加して持続的設定を可能にする。
67
+
68
+ #### (B) warning も exit 1 を default に保つ (CI 互換性優先)
69
+
70
+ 既存挙動を維持し、`failOnWarning: false` で warning-only fail を許容できるようにする。default を変えないため移行コストはゼロだが、`defaultSeverity: warning` の宣言と「常に fail として扱う」実装の semantic 乖離は残る。
71
+
72
+ ## Decision
73
+
74
+ **(A) を採用する。**
75
+
76
+ ### 採用理由
77
+
78
+ 1. **error catalog の宣言と実装を整合させる** — `defaultSeverity: warning` は「警告であって致命ではない」という宣言。実装側が常に fail として扱うのは catalog 宣言と矛盾しており、catalog を信頼できない状態を放置することは品質防御ツールとして自己矛盾
79
+ 2. **L4 validator の位置付けと整合** — ADR-001 (4層防御モデル) で L4 は advisory/observability layer として位置付けられており、drift / dead-code / freshness は「気付きを与える」ことが目的。これらが CI を止めるのは過剰
80
+ 3. **`failOnWarning: true` で旧挙動に opt-in 可能** — 厳格な CI を維持したい user は config flag または CLI flag で従来通りの挙動を選べる
81
+ 4. **config single source (ADR-007) の精神** — CLI flag のみで設定可能だった項目を config に持ち上げることで、CI スクリプトと config の再現可能性を担保
82
+ 5. **既存内部パラメータ名 `failOnWarning` と整合** — `AggregateResultsInput.failOnWarning` / `RunFullValidationInput.failOnWarning` / `ci-governance.PresetConfigAdapter.failOnWarning` が既に同名で通っている。schema 名と内部命名を揃えることで threading が単純
83
+
84
+ ### 集計セマンティクス(修正後)
85
+
86
+ ```ts
87
+ const hasNonWarningError = result.errors.some((e) => e.severity !== 'warning');
88
+ const hasWarnings = result.errors.some((e) => e.severity === 'warning');
89
+
90
+ // passed=false かつ errors=[] の防御的ケースは fail としてカウント(severity 判定不能のため安全側に倒す)
91
+ const isEmptyFail = !result.passed && result.errors.length === 0;
92
+
93
+ const hasFail =
94
+ !result.passed &&
95
+ (isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings));
96
+ ```
97
+
98
+ | 状態 | `failOnWarning=false` (default) | `failOnWarning=true` |
99
+ |------|---------------------------------|----------------------|
100
+ | `passed=true` (errors=[]) | PASS | PASS |
101
+ | `passed=false` + error-severity 含む | FAIL | FAIL |
102
+ | `passed=false` + warning-only | **PASS (新挙動)** | FAIL (従来挙動) |
103
+ | `passed=false` + errors=[] | FAIL (防御) | FAIL (防御) |
104
+
105
+ ### Config schema 拡張
106
+
107
+ `phasegate.config.json` に `validate` セクションを新設:
108
+
109
+ ```json
110
+ {
111
+ "validate": {
112
+ "failOnWarning": false
113
+ }
114
+ }
115
+ ```
116
+
117
+ - 型: `boolean`、default: `false`
118
+ - 旧挙動を維持したい user は `true` に設定
119
+ - CLI flag `--fail-on-warning` は config 値を override する(CLI > config の優先順位)
120
+
121
+ ### Formatter での severity 表示
122
+
123
+ human/agent/ci formatter で `[FAIL]` と `[WARN]` を分離:
124
+
125
+ - `result.passed === false` かつ errors が **error severity を含む** → `[FAIL]`
126
+ - `result.passed === false` かつ errors が **warning-only** → `[WARN]`
127
+ - `result.passed === true` → `[PASS]`
128
+ - `result.skipped === true` → `[SKIP]`
129
+
130
+ これにより output から severity が一目で読める。CI 解析スクリプトの後方互換のため、`agent` / `ci` フォーマットでは `severity` フィールドが既に JSON に含まれている (現状)。`[WARN]` 表示は human フォーマットを主対象とする。
131
+
132
+ ### 後方互換戦略
133
+
134
+ - **default 挙動の変更**: warning-only fail で exit 0 / overall PASS — これは BREAKING CHANGE
135
+ - **opt-in 経路**: `validate.failOnWarning: true` を config に追加するか、`--fail-on-warning` CLI flag で旧挙動を選べる
136
+ - **CHANGELOG**: `BREAKING` 認定で記載。ただし phasegate は `0.x` リリースのため、CLAUDE.md のバージョニングルールに従い minor bump (`0.130.0` → `0.131.0`) で出す(SemVer 上の major bump は phasegate `1.0.0` 到達時にまとめて宣言)
137
+ - **migration ガイド**: 「CI で warning も止めたかった user は `validate.failOnWarning: true` を設定する」を CHANGELOG / configuration.md に明記
138
+
139
+ ## Consequences
140
+
141
+ ### ポジティブ
142
+
143
+ - error catalog の `defaultSeverity` 宣言と実装挙動が一致し、catalog を信頼できる状態に戻る
144
+ - L4 advisory validator が CI を止めなくなり、drift / dead-code / freshness が日常的に観測しやすい運用形態になる
145
+ - `validate.failOnWarning` で CI 厳格度を config 経由で持続的に設定可能になり、CI スクリプト側の `--fail-on-warning` 散逸が解消
146
+ - formatter の `[WARN]` 表示で fail の severity が UX 上明確になる
147
+ - WI-091 reporter の plan-level workaround(false positive 注釈)が不要になる
148
+
149
+ ### ネガティブ / トレードオフ
150
+
151
+ - **既存 user CI への BREAKING**: warning-only fail を error 同等に扱っていた user の CI が「PASS / exit 0」に変わる。silent regression 化のリスク
152
+ - **緩和策**: CHANGELOG で BREAKING 認定 + migration ガイド明記。`validate.failOnWarning: true` で旧挙動に opt-in
153
+ - **回帰テスト負荷**: warning-only / error-only / mixed の 3 ケースを `aggregate-validation-results-usecase` の IT で網羅する必要
154
+ - **緩和策**: 既存テストヘルパで 3 ケースは小コストで追加可能
155
+ - **config schema v3 拡張**: 新セクション `validate` の追加。schema migration は不要だが、config-foundation の defaults / mapper を更新
156
+ - **緩和策**: `validate.failOnWarning` のみの新規追加。既存 schema を変えないため migration 不要
157
+
158
+ ### スコープ外(本 ADR で扱わない)
159
+
160
+ - error severity 自体のカタログ変更 (L4-XXX を warning から error に変える等) — 別 WI
161
+ - `defense preset` (`project.preset: minimal/standard/strict`) ごとの `failOnWarning` default 切り替え — 現状全 preset で `false` default。preset ごとに切り替える必要が出たら別 ADR
162
+ - WI-091 finding #4 (paths threading / WI-093) / #5 (pointers / WI-095) — 別 WI
163
+ - exit code を `0` / `1` 以外 (`2` 等) に拡張 — 本 ADR は warning と error の二値判定のみ
164
+
165
+ ## Migration
166
+
167
+ 1. **集計ロジック改修** (`aggregate-validation-results-usecase.ts:35-42`):
168
+ - `hasFail` 計算式を「Decision 集計セマンティクス」通りに変更
169
+ - 既存テスト (`__tests__/integration/validator-system/usecases/aggregate-validation-results-usecase.test.ts`) を新挙動に合わせて更新
170
+ 2. **回帰テスト追加**:
171
+ - warning-only fail で `overallPassed=true` / exit 0 になること
172
+ - error-severity fail で `overallPassed=false` / exit 1 になること
173
+ - mixed (warning + error) で `overallPassed=false` / exit 1 になること
174
+ - `failOnWarning=true` 時に warning-only でも `overallPassed=false` になること
175
+ 3. **config schema 拡張**:
176
+ - `HarnessConfigResolvedDocument` に `validate: { failOnWarning: boolean }` を追加
177
+ - `config-foundation` の defaults / loader / mapper で新キーをハンドル
178
+ - `toValidatorSystemConfig` で `validate.failOnWarning` を validator-system module に伝搬
179
+ - `createValidatorSystemModule` 経由で `RunValidatorsHandler` に渡す
180
+ 4. **formatter 改修**:
181
+ - `human-validation-result-formatter.ts` で warning-only fail を `[WARN]` 表示
182
+ - `agent-validation-result-formatter.ts` / `ci-validation-result-formatter.ts` も整合 (既存 `severity` field との重複を整理)
183
+ 5. **ドキュメント**:
184
+ - `docs/guide/layer-model.md` に severity policy セクション追加
185
+ - `docs/guide/configuration.md` に `validate.failOnWarning` 説明と migration ガイド
186
+ 6. **CHANGELOG**: BREAKING CHANGE として記載 (GitHub Issue [#4](https://github.com/junpei-9898/phasegate/issues/4) 参照付き)
187
+ 7. **dogfood**: drift 検出を意図的に発生させて exit code が `validate.failOnWarning` に従って切り替わることを確認
188
+
189
+ 詳細な実装は WI-094 で進める。
190
+
191
+ ## 関連
192
+
193
+ - **WI-094** — 本 ADR を駆動する Work Item。GitHub Issue [#4](https://github.com/junpei-9898/phasegate/issues/4) finding #2 に対応
194
+ - **WI-091** — WI-094 の親 Work Item。dogfood で finding #2 を検出
195
+ - **`scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts:35-42`** — 集計ロジックの修正対象
196
+ - **`scripts/harness/validator-system/presentation/handlers/report-validation-results-handler.ts:62`** — exit code 算出箇所
197
+ - **`scripts/harness/validator-system/domain/value-objects/error-catalog.ts`** — `defaultSeverity` 宣言の一次情報
198
+ - **`scripts/harness/main.ts:997-1008`** — CLI flag → handler への threading
199
+ - **`scripts/harness/config-foundation/domain/harness-config.ts`** — `HarnessConfigResolvedDocument` 拡張対象
200
+ - **`scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts`** — `toValidatorSystemConfig` 拡張対象
201
+ - **ADR-007** — harness-config Single Source of Truth。本 ADR は `failOnWarning` を CLI flag のみから config まで持ち上げて ADR-007 の精神を強化
202
+ - **ADR-001** — 4 層防御モデル。L4 validator の advisory 位置付けが本決定の根拠
@@ -405,6 +405,40 @@ Quick Mode with `relaxedGates: ["phase-gate"]` relaxes `storyReflection` as well
405
405
  | `format` | `string` | `"json"` | Output format for validation reports. |
406
406
  | `outputDir` | `string` | `"reports"` | Directory where reports are written. |
407
407
 
408
+ #### `validate` (severity policy, ADR-017 / WI-094)
409
+
410
+ Controls how warning-severity validator failures are aggregated into the overall result. Introduced in v0.131.0 to fix the dead-code `failOnWarning` parameter (`aggregate-validation-results-usecase.ts:35-42`) where warning-only fails were always counted as overall FAIL.
411
+
412
+ | Sub-field | Type | Default by preset | Description |
413
+ |------------------|-----------|------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
414
+ | `failOnWarning` | `boolean` | `false` (`minimal` / `standard`), `true` (`strict`) | When `true`, warning-only validator fails (e.g. L4-001 drift, L4-002 consistency, L4-003 dead-code) count as overall FAIL / exit 1. When `false`, they count as PASS / exit 0. |
415
+
416
+ CLI override: `--fail-on-warning` / `--no-fail-on-warning` (CLI > config). Both unspecified → config value used.
417
+
418
+ Resulting label in `human` formatter:
419
+
420
+ | validator state | `failOnWarning=false` | `failOnWarning=true` |
421
+ |-------------------------------------|-----------------------|----------------------|
422
+ | `passed=true` (errors=[]) | `[PASS]` | `[PASS]` |
423
+ | `passed=false` + `severity: error` | `[FAIL]` (overall FAIL) | `[FAIL]` (overall FAIL) |
424
+ | `passed=false` + warning-only | `[WARN]` (overall PASS) | `[WARN]` (overall FAIL) |
425
+ | `passed=false` + errors=[] (defensive) | `[FAIL]` (overall FAIL) | `[FAIL]` (overall FAIL) |
426
+ | `skipped=true` | `[SKIP]` | `[SKIP]` |
427
+
428
+ `agent` / `ci` formatter (JSON output) preserves the existing `severity` field on each error and is unchanged structurally — downstream parsers do not need to update.
429
+
430
+ ##### Migration from v0.130.0 and earlier
431
+
432
+ The fix is BREAKING for `minimal` / `standard` preset users who relied on warning-only fails stopping CI:
433
+
434
+ ```json
435
+ {
436
+ "validate": { "failOnWarning": true }
437
+ }
438
+ ```
439
+
440
+ `strict` preset already defaults to `failOnWarning: true`, matching ci-governance template adapter precedent — no migration needed.
441
+
408
442
  #### `baseline` (retrofit grandfather)
409
443
 
410
444
  Introduced in ISSUE-007 Wave 1 (v0.65.0) and wired into the pre-tool-use hook by Wave 2 (v0.66.0). When phasegate is added to an existing repository, the `baseline` block lets you snapshot the current state of the codebase so legacy files do not trip `phase-gate` on first edit. Files in the snapshot are exempted **until they are structurally modified** (sha1 mismatch); new files are subject to `phase-gate` from the start.
@@ -183,6 +183,25 @@ Error codes follow the pattern `L{layer}-{number}`:
183
183
 
184
184
  ---
185
185
 
186
+ ## Severity Policy (ADR-017 / WI-094, v0.131.0+)
187
+
188
+ Each error catalog entry declares a `defaultSeverity` of `error` or `warning`. As of v0.131.0, the aggregator (`aggregate-validation-results-usecase.ts`) honors this declaration when computing `overallPassed` and exit code:
189
+
190
+ - A validator returning **only `severity: warning` errors** is treated as **overall PASS** (exit 0) by default. The result is labeled `[WARN]` in `human` formatter so the warning is still visible.
191
+ - A validator returning at least one **`severity: error`** counts as **overall FAIL** (exit 1). Mixed warning + error fails are also FAIL because of the error.
192
+ - A validator returning `passed=false` with no errors is treated as FAIL (defensive — severity cannot be assessed).
193
+
194
+ **Validators with `defaultSeverity: warning`** (advisory by design — fail does not stop CI by default):
195
+ - L4-001 drift-detect
196
+ - L4-002 consistency-check
197
+ - L4-003 dead-code
198
+
199
+ **Opt-in to strict mode** via `phasegate.config.json` `validate.failOnWarning: true` or CLI `--fail-on-warning` (CLI > config). The `strict` preset defaults to `failOnWarning: true` to match the precedent set by the `ci-governance` preset adapter.
200
+
201
+ History: prior to v0.131.0, the aggregator's `failOnWarning` flag was effectively dead code (`hasFail = !result.passed || ...` masked it), so every warning-only fail produced exit 1 regardless of severity declaration. See ADR-017 for the rationale.
202
+
203
+ ---
204
+
186
205
  ## Presets and Layer Activation
187
206
 
188
207
  Presets control which layers are active. Choose based on project maturity and team discipline.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.130.0",
3
+ "version": "0.132.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit config-foundation
4
- * @work-item-id WI-092
4
+ * @work-item-id WI-092 / WI-094
5
5
  */
6
6
  import type { HarnessConfigV2 } from '../../domain/harness-config.js';
7
7
 
@@ -15,5 +15,8 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
15
15
  L3: { enabled: resolvedConfig.layers.L3.enabled },
16
16
  L4: { enabled: resolvedConfig.layers.L4.enabled },
17
17
  },
18
+ validate: {
19
+ failOnWarning: resolvedConfig.validate.failOnWarning,
20
+ },
18
21
  };
19
22
  }
@@ -13,6 +13,7 @@ import { PlanningModeConfig } from './value-objects/planning-mode-config.js';
13
13
  import { ProjectConfig } from './value-objects/project-config.js';
14
14
  import { QuickModeConfig } from './value-objects/quick-mode-config.js';
15
15
  import { ReportingConfig } from './value-objects/reporting-config.js';
16
+ import { ValidateConfig } from './value-objects/validate-config.js';
16
17
  import { HarnessesConfig } from './value-objects/harnesses-config.js';
17
18
  import type { FeatureNameValue } from './value-objects/feature-name.js';
18
19
  import type { FeatureName } from './value-objects/feature-name.js';
@@ -49,6 +50,7 @@ export interface HarnessConfigSourceDocument {
49
50
  harnesses: Partial<HarnessConfigResolvedDocument['harnesses']>;
50
51
  paths: HarnessConfigResolvedDocument['paths'];
51
52
  reporting: HarnessConfigResolvedDocument['reporting'];
53
+ validate?: HarnessConfigResolvedDocument['validate'];
52
54
  architecture?: ArchitectureConfigSource;
53
55
  }
54
56
 
@@ -108,6 +110,9 @@ export interface HarnessConfigResolvedDocument {
108
110
  format: string;
109
111
  outputDir: string;
110
112
  };
113
+ validate: {
114
+ failOnWarning: boolean;
115
+ };
111
116
  architecture?: ArchitectureConfigDocument;
112
117
  }
113
118
 
@@ -226,6 +231,7 @@ export class HarnessConfig {
226
231
  readonly planningMode: PlanningModeConfig;
227
232
  readonly paths: PathsConfig;
228
233
  readonly reporting: ReportingConfig;
234
+ readonly validate: ValidateConfig;
229
235
  harnesses: HarnessesConfig;
230
236
 
231
237
  private sourceDocument: HarnessConfigSourceDocument;
@@ -241,6 +247,7 @@ export class HarnessConfig {
241
247
  harnesses: HarnessesConfig;
242
248
  paths: PathsConfig;
243
249
  reporting: ReportingConfig;
250
+ validate: ValidateConfig;
244
251
  sourceDocument: HarnessConfigSourceDocument;
245
252
  resolvedDocument: HarnessConfigResolvedDocument;
246
253
  pendingEvents: readonly DomainEvent[];
@@ -253,6 +260,7 @@ export class HarnessConfig {
253
260
  this.harnesses = props.harnesses;
254
261
  this.paths = props.paths;
255
262
  this.reporting = props.reporting;
263
+ this.validate = props.validate;
256
264
  this.sourceDocument = deepClone(props.sourceDocument);
257
265
  this.resolvedDocument = deepClone(props.resolvedDocument);
258
266
  this.pendingEvents = [...props.pendingEvents];
@@ -271,6 +279,7 @@ export class HarnessConfig {
271
279
  const harnesses = HarnessesConfig.create(props.resolvedDocument.harnesses);
272
280
  const paths = PathsConfig.create(props.resolvedDocument.paths);
273
281
  const reporting = ReportingConfig.create(props.resolvedDocument.reporting);
282
+ const validate = ValidateConfig.create(props.resolvedDocument.validate);
274
283
 
275
284
  if (
276
285
  props.sourceDocument.project.preset !== props.resolvedDocument.project.preset
@@ -289,6 +298,7 @@ export class HarnessConfig {
289
298
  harnesses,
290
299
  paths,
291
300
  reporting,
301
+ validate,
292
302
  sourceDocument: props.sourceDocument,
293
303
  resolvedDocument: props.resolvedDocument,
294
304
  pendingEvents: props.pendingEvents ?? [],
@@ -18,6 +18,7 @@ export interface PresetDefinition {
18
18
  harnesses: HarnessConfigResolvedDocument['harnesses'];
19
19
  paths: HarnessConfigResolvedDocument['paths'];
20
20
  reporting: HarnessConfigResolvedDocument['reporting'];
21
+ validate: HarnessConfigResolvedDocument['validate'];
21
22
  }
22
23
 
23
24
  export class InvalidPresetDefinitionError extends ConfigFoundationDomainError {
@@ -113,6 +114,7 @@ function assertValidPresetDefinition(
113
114
  'harnesses',
114
115
  'paths',
115
116
  'reporting',
117
+ 'validate',
116
118
  ];
117
119
 
118
120
  for (const key of requiredKeys) {
@@ -177,6 +179,11 @@ export class PresetResolutionService {
177
179
  sourceDocument.reporting,
178
180
  'reporting',
179
181
  ),
182
+ validate: deepMerge(
183
+ presetDefinition.validate,
184
+ sourceDocument.validate,
185
+ 'validate',
186
+ ),
180
187
  };
181
188
  }
182
189
 
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit config-foundation
4
+ *
5
+ * バリデーション挙動設定を表す値オブジェクト (ADR-017 / WI-094)
6
+ * failOnWarning=true の場合、warning-only validator fail も overall FAIL として扱う
7
+ */
8
+ import { ConfigValidationError } from '../errors/config-validation-error.js';
9
+
10
+ export interface ValidateConfigProps {
11
+ readonly failOnWarning: boolean;
12
+ }
13
+
14
+ export class ValidateConfig {
15
+ readonly failOnWarning: boolean;
16
+
17
+ constructor(props: ValidateConfigProps) {
18
+ if (typeof props.failOnWarning !== 'boolean') {
19
+ throw new ConfigValidationError('failOnWarning は boolean でなければなりません');
20
+ }
21
+
22
+ this.failOnWarning = props.failOnWarning;
23
+ }
24
+
25
+ static create(raw: ValidateConfigProps): ValidateConfig {
26
+ return new ValidateConfig(raw);
27
+ }
28
+
29
+ equals(other: ValidateConfig): boolean {
30
+ return this.failOnWarning === other.failOnWarning;
31
+ }
32
+ }
@@ -46,5 +46,8 @@
46
46
  "reporting": {
47
47
  "format": "json",
48
48
  "outputDir": "reports"
49
+ },
50
+ "validate": {
51
+ "failOnWarning": false
49
52
  }
50
53
  }
@@ -46,5 +46,8 @@
46
46
  "reporting": {
47
47
  "format": "json",
48
48
  "outputDir": "reports"
49
+ },
50
+ "validate": {
51
+ "failOnWarning": false
49
52
  }
50
53
  }
@@ -46,5 +46,8 @@
46
46
  "reporting": {
47
47
  "format": "json",
48
48
  "outputDir": "reports"
49
+ },
50
+ "validate": {
51
+ "failOnWarning": true
49
52
  }
50
53
  }
@@ -455,6 +455,15 @@
455
455
  }
456
456
  }
457
457
  },
458
+ "validate": {
459
+ "type": "object",
460
+ "additionalProperties": false,
461
+ "properties": {
462
+ "failOnWarning": {
463
+ "type": "boolean"
464
+ }
465
+ }
466
+ },
458
467
  "protectedFiles": {
459
468
  "type": "object",
460
469
  "additionalProperties": false,
@@ -455,6 +455,15 @@
455
455
  }
456
456
  }
457
457
  },
458
+ "validate": {
459
+ "type": "object",
460
+ "additionalProperties": false,
461
+ "properties": {
462
+ "failOnWarning": {
463
+ "type": "boolean"
464
+ }
465
+ }
466
+ },
458
467
  "protectedFiles": {
459
468
  "type": "object",
460
469
  "additionalProperties": false,
@@ -174,6 +174,20 @@ function hasFlag(args: readonly string[], flag: string): boolean {
174
174
  return args.includes(flag);
175
175
  }
176
176
 
177
+ /**
178
+ * WI-094 / ADR-017: CLI で boolean フラグを tri-state に解釈する。
179
+ * - `--<flag>` 指定 → true
180
+ * - `--no-<flag>` 指定 → false
181
+ * - 両方未指定 → undefined (config 値にフォールバック)
182
+ * 両方同時指定された場合は後置を優先する。
183
+ */
184
+ function parseTriStateFlag(args: readonly string[], positiveFlag: string, negativeFlag: string): boolean | undefined {
185
+ const positiveIdx = args.lastIndexOf(positiveFlag);
186
+ const negativeIdx = args.lastIndexOf(negativeFlag);
187
+ if (positiveIdx === -1 && negativeIdx === -1) return undefined;
188
+ return positiveIdx > negativeIdx;
189
+ }
190
+
177
191
  function levenshtein(a: string, b: string): number {
178
192
  const m = a.length;
179
193
  const n = b.length;
@@ -994,7 +1008,8 @@ async function main(): Promise<void> {
994
1008
  const unit = parseFlag(args, "--unit");
995
1009
  const phase = parseFlag(args, "--phase");
996
1010
  const format = parseFlag(args, "--format") as "human" | "agent" | "ci" | undefined;
997
- const failOnWarning = hasFlag(args, "--fail-on-warning");
1011
+ // WI-094 / ADR-017: --fail-on-warning / --no-fail-on-warning / 未指定→config値
1012
+ const failOnWarning = parseTriStateFlag(args, "--fail-on-warning", "--no-fail-on-warning");
998
1013
  const noL4 = hasFlag(args, "--no-l4");
999
1014
  const targetPaths = parsePositionalArgs(args.slice(1), ["--layer", "--unit", "--phase", "--format"]);
1000
1015
  const result = await mod.handlers.runValidators.execute({
@@ -32,8 +32,12 @@ export class AggregateValidationResultsUseCase {
32
32
  continue;
33
33
  }
34
34
 
35
+ // ADR-017: warning-only validator fail は failOnWarning=false で overall PASS、true で従来挙動 (FAIL)
36
+ const hasNonWarningError = result.errors.some((e) => e.severity !== 'warning');
35
37
  const hasWarnings = result.errors.some((e) => e.severity === 'warning');
36
- const hasFail = !result.passed || (failOnWarning && hasWarnings);
38
+ const isEmptyFail = !result.passed && result.errors.length === 0;
39
+ const hasFail =
40
+ !result.passed && (isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings));
37
41
 
38
42
  if (hasFail) {
39
43
  failedValidators++;
@@ -50,6 +50,7 @@ const DEFAULT_CONFIG = {
50
50
  L3: { enabled: true, validators: ['L3-001', 'L3-002', 'L3-003', 'L3-004'], coverageThreshold: 90, bundleSizeLimit: 512000 },
51
51
  L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003'] },
52
52
  },
53
+ validate: { failOnWarning: false },
53
54
  };
54
55
 
55
56
  /** バリデータ定義カタログ(全10件) */
@@ -111,6 +112,8 @@ export interface ValidatorSystemModule {
111
112
  export function createValidatorSystemModule(config?: object): ValidatorSystemModule {
112
113
  const configData = (config ?? DEFAULT_CONFIG) as typeof DEFAULT_CONFIG;
113
114
  const configPort = new HarnessConfigValidatorConfigAdapter(configData);
115
+ // WI-094 / ADR-017: validate.failOnWarning を handler に伝搬。config 未指定時は false
116
+ const defaultFailOnWarning = configData.validate?.failOnWarning ?? false;
114
117
  const registry = buildDefaultRegistry();
115
118
  const executionService = new ValidatorExecutionService({ configPort });
116
119
  const contractMapper = new ValidationResultContractMapper();
@@ -215,7 +218,12 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
215
218
  });
216
219
 
217
220
  const handlers = {
218
- runValidators: new RunValidatorsHandler({ runFullValidationUseCase, runL0ValidatorsUseCase, runL1ValidatorsUseCase }),
221
+ runValidators: new RunValidatorsHandler({
222
+ runFullValidationUseCase,
223
+ runL0ValidatorsUseCase,
224
+ runL1ValidatorsUseCase,
225
+ defaultFailOnWarning,
226
+ }),
219
227
  runQuickMode: new RunQuickModeHandler({ runQuickModeUseCase }),
220
228
  reportValidationResults: new ReportValidationResultsHandler(),
221
229
  };
@@ -43,10 +43,11 @@ export class ConsistencyReport {
43
43
  return this.mismatchPairs.length;
44
44
  }
45
45
 
46
+ // ADR-017 / WI-094: error catalog の defaultSeverity: warning と整合
46
47
  toHarnessErrors(): readonly HarnessErrorLike[] {
47
48
  return this.mismatchPairs.map((pair) => ({
48
49
  code: { value: 'L4-002', toString: () => 'L4-002' },
49
- severity: { value: 'error', toString: () => 'error' },
50
+ severity: { value: 'warning', toString: () => 'warning' },
50
51
  message: `レイヤー整合性違反: expected "${pair.expected}" but got "${pair.actual}" at ${pair.location}`,
51
52
  suggestion: '設計文書間のレイヤー依存方向を統一してください',
52
53
  }));
@@ -38,12 +38,13 @@ export class DeadCodeReport {
38
38
  return this.unusedExports.length > 0 || this.unreachableCode.length > 0;
39
39
  }
40
40
 
41
+ // ADR-017 / WI-094: error catalog の defaultSeverity: warning と整合
41
42
  toHarnessErrors(): readonly HarnessErrorLike[] {
42
43
  const errors: HarnessErrorLike[] = [];
43
44
  for (const exportId of this.unusedExports) {
44
45
  errors.push({
45
46
  code: { value: 'L4-003', toString: () => 'L4-003' },
46
- severity: { value: 'error', toString: () => 'error' },
47
+ severity: { value: 'warning', toString: () => 'warning' },
47
48
  message: `未使用エクスポート: ${exportId}`,
48
49
  suggestion: '未使用エクスポートを削除するか、他ファイルからimportしてください',
49
50
  });
@@ -51,7 +52,7 @@ export class DeadCodeReport {
51
52
  for (const loc of this.unreachableCode) {
52
53
  errors.push({
53
54
  code: { value: 'L4-003', toString: () => 'L4-003' },
54
- severity: { value: 'error', toString: () => 'error' },
55
+ severity: { value: 'warning', toString: () => 'warning' },
55
56
  message: `到達不能コード: ${loc.filePath}:${loc.range.startLine}-${loc.range.endLine}`,
56
57
  suggestion: '到達不能なコードブロックを削除してください',
57
58
  });
@@ -42,9 +42,10 @@ export class DriftReport {
42
42
  }
43
43
 
44
44
  toHarnessError(): HarnessErrorLike {
45
+ // ADR-017 / WI-094: error catalog の defaultSeverity: warning と整合
45
46
  return {
46
47
  code: { value: 'L4-001', toString: () => 'L4-001' },
47
- severity: { value: 'error', toString: () => 'error' },
48
+ severity: { value: 'warning', toString: () => 'warning' },
48
49
  message: `乖離検出 [${this.direction}] Unit: ${this.unitName}, Element: ${this.element}`,
49
50
  suggestion: this.recommendation,
50
51
  };
@@ -15,7 +15,15 @@ export class HumanValidationResultFormatter {
15
15
  lines.push('');
16
16
 
17
17
  for (const result of report.results) {
18
- const status = result.skipped ? 'SKIP' : result.passed ? 'PASS' : 'FAIL';
18
+ // WI-094 / ADR-017: warning-only fail [WARN]、error severity を含む fail は [FAIL]
19
+ const hasNonWarningError = result.errors.some((e) => e.severity !== 'warning');
20
+ const status = result.skipped
21
+ ? 'SKIP'
22
+ : result.passed
23
+ ? 'PASS'
24
+ : hasNonWarningError || result.errors.length === 0
25
+ ? 'FAIL'
26
+ : 'WARN';
19
27
  lines.push(`[${status}] ${result.validatorId} (${result.durationMs}ms)`);
20
28
  for (const error of result.errors) {
21
29
  lines.push(` ⚠ ${error.message}`);
@@ -27,17 +27,21 @@ export interface RunValidatorsHandlerDeps {
27
27
  runFullValidationUseCase: RunFullValidationUseCase;
28
28
  runL0ValidatorsUseCase?: RunL0ValidatorsUseCase;
29
29
  runL1ValidatorsUseCase?: RunL1ValidatorsUseCase;
30
+ /** WI-094 / ADR-017: config 由来の failOnWarning デフォルト。args.failOnWarning が undefined の場合に適用 */
31
+ defaultFailOnWarning?: boolean;
30
32
  }
31
33
 
32
34
  export class RunValidatorsHandler {
33
35
  private readonly useCase: RunFullValidationUseCase;
34
36
  private readonly l0UseCase: RunL0ValidatorsUseCase | undefined;
35
37
  private readonly l1UseCase: RunL1ValidatorsUseCase | undefined;
38
+ private readonly defaultFailOnWarning: boolean;
36
39
 
37
40
  constructor(deps: RunValidatorsHandlerDeps) {
38
41
  this.useCase = deps.runFullValidationUseCase;
39
42
  this.l0UseCase = deps.runL0ValidatorsUseCase;
40
43
  this.l1UseCase = deps.runL1ValidatorsUseCase;
44
+ this.defaultFailOnWarning = deps.defaultFailOnWarning ?? false;
41
45
  }
42
46
 
43
47
  async execute(args: RunValidatorsHandlerArgs): Promise<{ output: string; exitCode: number }> {
@@ -118,12 +122,13 @@ export class RunValidatorsHandler {
118
122
  else if (args.layer === 'all') targetLayers = ['L2', 'L3', 'L4'];
119
123
  // undefined → フィルタなし(従来挙動)
120
124
 
125
+ const failOnWarning = args.failOnWarning ?? this.defaultFailOnWarning;
121
126
  const report = await this.useCase.execute({
122
127
  targetPaths: args.targetPaths ?? [],
123
128
  unitName: args.unit ?? '',
124
129
  currentPhase: args.phase ?? '',
125
130
  includeL4: !args.noL4,
126
- failOnWarning: args.failOnWarning,
131
+ failOnWarning,
127
132
  targetLayers,
128
133
  });
129
134