phasegate 0.161.0 → 0.163.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 +16 -0
- package/docs/contracts/requirement-test-matrix.schema.json +1 -1
- package/package.json +6 -3
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +91 -23
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +4 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/ci-governance-baseline-grandfather-adapter.ts +25 -2
- package/scripts/harness/agent-integration/infrastructure/adapters/phase-gate-query-adapter.ts +17 -2
- package/scripts/harness/biome-ast-engine/domain/value-objects/import-graph.ts +28 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +1 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +1 -0
- package/scripts/harness/config-foundation/domain/value-objects/l3-config.ts +4 -0
- package/scripts/harness/config-foundation/domain/value-objects/layers-config.ts +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +14 -9
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +14 -9
- package/scripts/harness/harness-api/infrastructure/adapters/phase-dependency-model-query-adapter.ts +18 -4
- package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +27 -4
- package/scripts/harness/installation/application/ports/model-delegation-port.ts +10 -0
- package/scripts/harness/installation/application/usecases/run-install.ts +56 -7
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +12 -7
- package/scripts/harness/installation/composition-root.ts +4 -2
- package/scripts/harness/installation/infrastructure/adapters/skill-deployer-model-delegation-adapter.ts +16 -0
- package/scripts/harness/installation/presentation/cli/install-handler.ts +2 -1
- package/scripts/harness/main.ts +68 -12
- package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +9 -7
- package/scripts/harness/nyquist-validation/domain/services/matrix-validation-service.ts +27 -2
- package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +4 -4
- package/scripts/harness/nyquist-validation/domain/value-objects/intent-coverage.ts +33 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +3 -1
- package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +4 -1
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +8 -3
- package/scripts/harness/quick-mode/composition-root.ts +3 -0
- package/scripts/harness/quick-mode/infrastructure/adapters/validator-system-quick-mode-execution-adapter.ts +18 -0
- package/scripts/harness/quick-mode/presentation/handlers/ci-check-quick-mode-handler.ts +51 -5
- package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +12 -2
- package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +12 -2
- package/scripts/harness/traceability-model/application/usecases/apply-work-item-status-usecase.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/parsers/story-catalog-parser.ts +28 -9
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +65 -27
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +6 -7
- package/scripts/harness/validator-system/composition-root.ts +8 -1
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +16 -6
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-work-item-reflection-adapter.ts +10 -1
- package/scripts/harness/validator-system/infrastructure/adapters/import-graph-source-analysis-adapter.ts +35 -3
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +86 -14
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +29 -3
- package/templates/.claude/scripts/deny-check.sh +73 -12
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.162.0] - 2026-07-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-221 — second-wave audit remediation: make the defenses real** — closes the remaining defects from the 2026-07-04 security audit where "advertised gates were not actually enforced". Product-default strictness is unchanged; where phasegate's own repo could not yet meet a now-real gate, it is configured honestly with a documented ratchet (see `docs/inception/_shared/l3-004-traceability-ratchet.md`).
|
|
15
|
+
- **フェーズゲート残存穴**: `BashWriteTargetExtractor` が `>|` / `dd of=` / `install` / `rsync` / `bash -c`・`sh -c`(ネスト再帰)の書き込みベクトルを検出するよう拡張。`.phasegate/baseline.json` を保護対象に追加し、grandfather 判定をパス一致のみから **sha1 整合検証**へ強化(手書き追記による素通りを封鎖)。
|
|
16
|
+
- **fail-open → fail-closed**: `phase-gate-query` / `phase-dependency-phase-gate-policy` / `phase-dependency-model-query` / skill-quality L1・L2 / nyquist-ac-coverage の各アダプタで、例外を握り潰して「合格」を返していた挙動を fail-closed 化(エラーを表面化し、防御が静かに開かないようにした)。
|
|
17
|
+
- **不発・誤発火バリデータ**: L4-003 dead-code の `.js`→`.ts/.tsx`(`.mjs/.cjs/.jsx`・index)解決で誤検出約1592件を解消。L4-002/WI 反映チェックが標準レイアウトで常時スキップされる不具合を修正。セキュリティスキャナの allowlist をファイル全体スキップから行スコープへ。`ci-check --quick` にバリデータを配線。`NEW_DOMAIN` の `changeKind` ハードコードを create/modify 判定に修正。
|
|
18
|
+
- **config スキーマ不整合(CLI 文鎮化)**: `storyReflection.mappings` のスキーマをコード/ドキュメントの `{inception, product, required}` に整合。`config:plan --intent l4-strict --apply`(および `ci-fail-on-warning`)が実消費キー `/validate/failOnWarning` を出力するよう修正。
|
|
19
|
+
- **Nyquist トレーサビリティ**: L3-003 カバレッジを実配線(`coverageReportPort`)し閾値を実効化。legacy StoryId エイリアス解決・`HF2-01` 形式・`MatrixValidationService` no-op を修正。domain→application の依存方向逆転を解消。
|
|
20
|
+
- **L3-003 カバレッジ・セマンティクス精緻化**: カバレッジ閾値未設定時はスキップ(オプトイン)、設定済み×レポート欠如/不足時は fail-closed、単一バリデータの例外が全体をクラッシュさせないよう per-validator 化。
|
|
21
|
+
- **L3-004 実バグ修正 + パス配線**: AC 網羅ゲートの story レジストリ空スタブを実レジストリ(traceability-model)へ配線し、`layers.L3.requirementMatrixPath`(既定 `.harness/requirement-test-matrix.json`)を新設して ci-check がマトリクスパスを供給するようにした(従来は全ユーザーで L3-004 が満たせなかった)。
|
|
22
|
+
- **シェル・移植性**: `deny-check.sh` のコマンド連結回避・world-readable ログ・`jq` 不在時 fail-open・glob 変換の部分適用を修正。Windows パス区切りでの WI 反映スキャン/採番不具合と 3桁固定の採番破綻を修正。import-graph substring フォールバックのレイヤー違反見逃しをセグメント境界一致で修正。`work-items --apply` の `completed`→`tested` 降格を防止。personal install の既存 pre-commit 無警告スキップと JSON マージのキー順依存を修正。
|
|
23
|
+
- **アーキ整合**: import-graph 修正で顕在化した `installation/application → setup/skill-deployer`(infrastructure)の禁止依存を、port(`ModelDelegationPort`)+ アダプタ + DI で解消。
|
|
24
|
+
- **カバレッジ計測基盤**: `@vitest/coverage-v8` を導入し、エントリポイント/サブプロセス専用ファイルを分母から原則除外した上で自リポの実カバレッジ **90.2%** を確認。`coverage` / `ci` スクリプトでレポート生成→検査の循環を解消(`coverage/` は gitignore)。
|
|
25
|
+
|
|
10
26
|
## [0.161.0] - 2026-07-04
|
|
11
27
|
|
|
12
28
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phasegate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.163.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",
|
|
@@ -59,7 +59,9 @@
|
|
|
59
59
|
"harness:disable": "pnpm phasegate:disable",
|
|
60
60
|
"harness:check-phase": "pnpm phasegate:check-phase",
|
|
61
61
|
"harness:check-ready": "pnpm phasegate:check-ready",
|
|
62
|
-
"test": "vitest run --config scripts/harness/__tests__/vitest.config.forks.ts && vitest run --config scripts/harness/__tests__/vitest.config.ts"
|
|
62
|
+
"test": "vitest run --config scripts/harness/__tests__/vitest.config.forks.ts && vitest run --config scripts/harness/__tests__/vitest.config.ts",
|
|
63
|
+
"coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && vitest run --config scripts/harness/__tests__/vitest.config.coverage.forks.ts --reporter=blob --outputFile=coverage/.blob/forks.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --reporter=blob --outputFile=coverage/.blob/threads.json && vitest run --config scripts/harness/__tests__/vitest.config.coverage.ts --merge-reports=coverage/.blob",
|
|
64
|
+
"ci": "npm run coverage && npm run test"
|
|
63
65
|
},
|
|
64
66
|
"dependencies": {
|
|
65
67
|
"ajv": "^8.18.0",
|
|
@@ -69,8 +71,9 @@
|
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|
|
71
73
|
"@biomejs/biome": "^2.4.7",
|
|
72
|
-
"@types/picomatch": "^4.0.0",
|
|
73
74
|
"@types/node": "^25.3.5",
|
|
75
|
+
"@types/picomatch": "^4.0.0",
|
|
76
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
74
77
|
"typescript": "^5.0.0",
|
|
75
78
|
"vitest": "^3.0.0"
|
|
76
79
|
},
|
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
* Phase Gate フックが Bash 経由のファイル書き込みを検知して保護するための基盤。
|
|
9
9
|
*
|
|
10
10
|
* 副作用なし・純粋関数相当。対応パターンは以下:
|
|
11
|
-
* - リダイレクト `>` / `>>`
|
|
11
|
+
* - リダイレクト `>` / `>>` / `>|` (clobber)
|
|
12
12
|
* - heredoc (`<<EOF > path`)
|
|
13
13
|
* - `tee` / `tee -a`
|
|
14
14
|
* - `sed -i` / `sed -i ''` (BSD)
|
|
15
15
|
* - `cp` / `mv` (宛先のみ)
|
|
16
16
|
* - `touch`
|
|
17
|
+
* - `dd of=<path>`
|
|
18
|
+
* - `install [opts] SRC... DEST` (coreutils, ファイル作成)
|
|
19
|
+
* - `rsync [opts] SRC... DEST`
|
|
20
|
+
* - `bash -c '...'` / `sh -c '...'` (ネストしたコマンドを再帰解析)
|
|
17
21
|
* - 複合コマンド (`&&`, `;`, `||`) とパイプ (`|`) 分割
|
|
18
22
|
* - ダブル/シングルクォート対応
|
|
19
23
|
* - `apply_patch` ヒアドキュメント (`*** Begin Patch` / `*** End Patch` ブロック内の
|
|
@@ -44,7 +48,11 @@ function tokenize(input: string): Token[] {
|
|
|
44
48
|
// 演算子をそのままトークン化
|
|
45
49
|
if (ch === '>' || ch === '<' || ch === '|' || ch === ';' || ch === '&') {
|
|
46
50
|
let op = ch;
|
|
47
|
-
|
|
51
|
+
// `>|` (clobber redirect) は独立した 2 文字演算子として扱う
|
|
52
|
+
if (ch === '>' && i + 1 < len && input[i + 1] === '|') {
|
|
53
|
+
op = '>|';
|
|
54
|
+
i += 2;
|
|
55
|
+
} else if (i + 1 < len && input[i + 1] === ch && (ch === '>' || ch === '<' || ch === '|' || ch === '&')) {
|
|
48
56
|
op = ch + ch;
|
|
49
57
|
i += 2;
|
|
50
58
|
} else {
|
|
@@ -139,10 +147,10 @@ function getCommandName(tokens: Token[]): string | undefined {
|
|
|
139
147
|
function extractFromSingleCommand(tokens: Token[]): string[] {
|
|
140
148
|
const results: string[] = [];
|
|
141
149
|
|
|
142
|
-
// 1) リダイレクト `>` / `>>` の右辺 (heredoc の `>` もこのパスで拾える)
|
|
150
|
+
// 1) リダイレクト `>` / `>>` / `>|` の右辺 (heredoc の `>` もこのパスで拾える)
|
|
143
151
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
144
152
|
const t = tokens[i];
|
|
145
|
-
if (t.quoted === 'none' && (t.value === '>' || t.value === '>>')) {
|
|
153
|
+
if (t.quoted === 'none' && (t.value === '>' || t.value === '>>' || t.value === '>|')) {
|
|
146
154
|
const next = tokens[i + 1];
|
|
147
155
|
if (next !== undefined) {
|
|
148
156
|
results.push(next.value);
|
|
@@ -213,14 +221,89 @@ function extractFromSingleCommand(tokens: Token[]): string[] {
|
|
|
213
221
|
for (let i = 1; i < tokens.length; i += 1) {
|
|
214
222
|
const t = tokens[i];
|
|
215
223
|
if (t.quoted === 'none' && t.value.startsWith('-')) continue;
|
|
216
|
-
if (t.quoted === 'none' && (t.value === '>' || t.value === '>>' || t.value === '|' || t.value === '<')) break;
|
|
224
|
+
if (t.quoted === 'none' && (t.value === '>' || t.value === '>>' || t.value === '>|' || t.value === '|' || t.value === '<')) break;
|
|
217
225
|
results.push(t.value);
|
|
218
226
|
}
|
|
227
|
+
} else if (baseName === 'dd') {
|
|
228
|
+
// `dd of=<path>` — 出力ファイルは `of=` オペランド
|
|
229
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
230
|
+
const t = tokens[i];
|
|
231
|
+
if (t.value.startsWith('of=')) {
|
|
232
|
+
const dest = t.value.slice('of='.length);
|
|
233
|
+
if (dest.length > 0) results.push(dest);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else if (baseName === 'install') {
|
|
237
|
+
// `install [opts] SRC... DEST` — 宛先は最後の非オプション引数 (coreutils はファイルを作成する)
|
|
238
|
+
const positionals = collectPositionals(tokens);
|
|
239
|
+
if (positionals.length >= 1) {
|
|
240
|
+
// `install -d DIR...` はディレクトリ作成のみだが保護側に倒し全ディレクトリを対象化
|
|
241
|
+
const isDirMode = tokens.some(
|
|
242
|
+
(t) => t.quoted === 'none' && (t.value === '-d' || t.value === '--directory'),
|
|
243
|
+
);
|
|
244
|
+
if (isDirMode) {
|
|
245
|
+
for (const p of positionals) results.push(p.value);
|
|
246
|
+
} else if (positionals.length >= 2) {
|
|
247
|
+
results.push(positionals[positionals.length - 1].value);
|
|
248
|
+
} else {
|
|
249
|
+
// SRC 省略で DEST のみ渡された不正形でも保護側に倒す
|
|
250
|
+
results.push(positionals[positionals.length - 1].value);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else if (baseName === 'rsync') {
|
|
254
|
+
// `rsync [opts] SRC... DEST` — 宛先は最後の非オプション引数
|
|
255
|
+
const positionals = collectPositionals(tokens);
|
|
256
|
+
if (positionals.length >= 2) {
|
|
257
|
+
results.push(positionals[positionals.length - 1].value);
|
|
258
|
+
}
|
|
259
|
+
} else if (baseName === 'bash' || baseName === 'sh') {
|
|
260
|
+
// `bash -c '<nested command>'` — ネストしたコマンドを再帰解析
|
|
261
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
262
|
+
const t = tokens[i];
|
|
263
|
+
if (t.quoted === 'none' && t.value === '-c') {
|
|
264
|
+
const script = tokens[i + 1];
|
|
265
|
+
if (script !== undefined) {
|
|
266
|
+
for (const nested of extractFromCommandString(script.value)) {
|
|
267
|
+
results.push(nested);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
219
273
|
}
|
|
220
274
|
|
|
221
275
|
return results;
|
|
222
276
|
}
|
|
223
277
|
|
|
278
|
+
/** コマンド先頭 (cmd 名) を除いた非オプション位置引数を収集する。リダイレクト境界で停止。 */
|
|
279
|
+
function collectPositionals(tokens: Token[]): Token[] {
|
|
280
|
+
const positionals: Token[] = [];
|
|
281
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
282
|
+
const t = tokens[i];
|
|
283
|
+
if (t.quoted === 'none' && t.value.startsWith('-')) continue;
|
|
284
|
+
if (t.quoted === 'none' && (t.value === '>' || t.value === '>>' || t.value === '>|' || t.value === '|' || t.value === '<')) break;
|
|
285
|
+
positionals.push(t);
|
|
286
|
+
}
|
|
287
|
+
return positionals;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** コマンド文字列を token 化 → 演算子分割 → 各コマンドから書き込み先を抽出する内部関数。 */
|
|
291
|
+
function extractFromCommandString(command: string): string[] {
|
|
292
|
+
const tokens = tokenize(command);
|
|
293
|
+
const groups = splitByOperators(tokens);
|
|
294
|
+
const collected: string[] = [];
|
|
295
|
+
for (const group of groups) {
|
|
296
|
+
for (const path of extractFromSingleCommand(group)) {
|
|
297
|
+
collected.push(path);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// ネスト内の apply_patch heredoc も拾う
|
|
301
|
+
for (const p of extractApplyPatchTargets(command)) {
|
|
302
|
+
collected.push(p);
|
|
303
|
+
}
|
|
304
|
+
return collected;
|
|
305
|
+
}
|
|
306
|
+
|
|
224
307
|
/** apply_patch ブロック境界マーカー (Begin/End) */
|
|
225
308
|
const APPLY_PATCH_BEGIN_SOURCE = String.raw`\*\*\*\s+Begin\s+Patch`;
|
|
226
309
|
const APPLY_PATCH_END_SOURCE = String.raw`\*\*\*\s+End\s+Patch`;
|
|
@@ -284,24 +367,9 @@ export class BashWriteTargetExtractor {
|
|
|
284
367
|
return Object.freeze([]);
|
|
285
368
|
}
|
|
286
369
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const collected: string[] = [];
|
|
291
|
-
for (const group of groups) {
|
|
292
|
-
const found = extractFromSingleCommand(group);
|
|
293
|
-
for (const path of found) {
|
|
294
|
-
collected.push(path);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// apply_patch ヒアドキュメント対応 (ISSUE-013 Wave 1)
|
|
299
|
-
// 既存の token ベース抽出では heredoc body 内のマーカー行を拾えないため、
|
|
300
|
-
// raw command 文字列から独立にスキャンする。
|
|
301
|
-
const applyPatchTargets = extractApplyPatchTargets(command);
|
|
302
|
-
for (const p of applyPatchTargets) {
|
|
303
|
-
collected.push(p);
|
|
304
|
-
}
|
|
370
|
+
// token 化 → 演算子分割 → 各コマンド抽出 + apply_patch ヒアドキュメント対応
|
|
371
|
+
// (ISSUE-013 Wave 1)。bash -c '...' 等のネストコマンドは内部で再帰解析される。
|
|
372
|
+
const collected = extractFromCommandString(command);
|
|
305
373
|
|
|
306
374
|
// 重複除去 (挿入順保持)
|
|
307
375
|
const seen = new Set<string>();
|
|
@@ -21,6 +21,10 @@ const DEFAULT_PATTERNS = [
|
|
|
21
21
|
'tsconfig.json',
|
|
22
22
|
'package.json',
|
|
23
23
|
'package-lock.json',
|
|
24
|
+
// baseline.json は grandfather 判定の信頼基盤。手動追記による protected file の
|
|
25
|
+
// grandfather bypass を防ぐため、書き込み自体を保護対象とする。
|
|
26
|
+
'.phasegate/baseline.json',
|
|
27
|
+
'**/.phasegate/baseline.json',
|
|
24
28
|
];
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
import type { ConfigQueryPort } from '../../domain/ports/config-query-port.js';
|
|
9
9
|
import type { BaselineRepositoryPort } from '../../../ci-governance/domain/ports/baseline-repository-port.js';
|
|
10
10
|
import { BaselineJsonRepositoryAdapter } from '../../../ci-governance/infrastructure/adapters/baseline-json-repository-adapter.js';
|
|
11
|
+
import type { FileHasherPort } from '../../../ci-governance/domain/ports/file-hasher-port.js';
|
|
12
|
+
import { FileSystemSha1HasherAdapter } from '../../../ci-governance/infrastructure/adapters/file-system-sha1-hasher-adapter.js';
|
|
11
13
|
|
|
12
14
|
export interface CiGovernanceBaselineGrandfatherAdapterDeps {
|
|
13
15
|
readonly baseDir: string;
|
|
@@ -16,6 +18,7 @@ export interface CiGovernanceBaselineGrandfatherAdapterDeps {
|
|
|
16
18
|
baseDir: string,
|
|
17
19
|
relativePath: string,
|
|
18
20
|
) => BaselineRepositoryPort;
|
|
21
|
+
readonly fileHasherFactory?: (baseDir: string) => FileHasherPort;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export class CiGovernanceBaselineGrandfatherAdapter
|
|
@@ -68,11 +71,31 @@ export class CiGovernanceBaselineGrandfatherAdapter
|
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
// 整合性検証: baseline に path が載っているだけでは grandfather しない。
|
|
75
|
+
// 記録済み sha1 と実ファイルの sha1 が一致する場合のみ grandfather 扱いとする。
|
|
76
|
+
// これにより baseline.json への手動 path 追記による bypass を防ぐ
|
|
77
|
+
//(追記側は実ファイルの正しい sha1 を予測できず、改変後の content とも一致しない)。
|
|
78
|
+
const hasherFactory =
|
|
79
|
+
this.deps.fileHasherFactory ??
|
|
80
|
+
((baseDir) => new FileSystemSha1HasherAdapter(baseDir));
|
|
81
|
+
const hasher = hasherFactory(this.deps.baseDir);
|
|
82
|
+
const entryByPath = new Map(snapshot.entries.map((e) => [e.path, e.sha1] as const));
|
|
83
|
+
|
|
71
84
|
const grandfathered: string[] = [];
|
|
72
85
|
for (const p of targetFilePaths) {
|
|
73
|
-
|
|
86
|
+
const recordedSha1 = entryByPath.get(p);
|
|
87
|
+
if (recordedSha1 === undefined) continue;
|
|
88
|
+
let actualSha1: string;
|
|
89
|
+
try {
|
|
90
|
+
actualSha1 = await hasher.hashFile(p);
|
|
91
|
+
} catch {
|
|
92
|
+
// ファイル読み取り失敗 (未作成・権限等) は grandfather 不可扱い (保護側に倒す)
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (actualSha1 === recordedSha1) grandfathered.push(p);
|
|
74
96
|
}
|
|
75
|
-
const allGrandfathered =
|
|
97
|
+
const allGrandfathered =
|
|
98
|
+
targetFilePaths.length > 0 && grandfathered.length === targetFilePaths.length;
|
|
76
99
|
|
|
77
100
|
return {
|
|
78
101
|
allGrandfathered,
|
package/scripts/harness/agent-integration/infrastructure/adapters/phase-gate-query-adapter.ts
CHANGED
|
@@ -39,13 +39,28 @@ export class PhaseGateQueryAdapter implements PhaseGateQueryPort {
|
|
|
39
39
|
return PhaseGateQueryResult.create(false, [result.text], []);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// Fail-closed: an unexpected exit code means the gate could not be evaluated.
|
|
43
|
+
// A quality gate that cannot confirm PASS must block, not open.
|
|
44
|
+
console.error(`[agent-integration] phase gate check returned unexpected exit code ${result.exitCode}`);
|
|
45
|
+
return PhaseGateQueryResult.create(
|
|
46
|
+
false,
|
|
47
|
+
[`phase gate check returned unexpected exit code ${result.exitCode}; treating as NOT passed (fail-closed)`],
|
|
48
|
+
[]
|
|
49
|
+
);
|
|
43
50
|
} catch (error) {
|
|
44
51
|
if (error instanceof ConfigValidationError) {
|
|
45
52
|
return PhaseGateQueryResult.create(false, [error.message], []);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
// Fail-closed: if phase-dependency-model could not be loaded/executed we cannot
|
|
56
|
+
// confirm the gate passed, so treat it as blocked rather than silently opening it.
|
|
57
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
58
|
+
console.error(`[agent-integration] phase gate check failed to run: ${message}`);
|
|
59
|
+
return PhaseGateQueryResult.create(
|
|
60
|
+
false,
|
|
61
|
+
[`phase gate check could not be evaluated (phase-dependency-model unavailable): ${message}`],
|
|
62
|
+
[]
|
|
63
|
+
);
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
|
|
@@ -46,6 +46,13 @@ const globToRegex = (pattern: string): RegExp => {
|
|
|
46
46
|
const next = pattern[index + 1];
|
|
47
47
|
|
|
48
48
|
if (current === '*' && next === '*') {
|
|
49
|
+
// A `**/` segment matches any number of leading path segments, INCLUDING
|
|
50
|
+
// none, so `**/shared-kernel/**` still matches `shared-kernel/a.ts`.
|
|
51
|
+
if (pattern[index + 2] === '/') {
|
|
52
|
+
output += '(?:.*/)?';
|
|
53
|
+
index += 2;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
49
56
|
output += '.*';
|
|
50
57
|
index += 1;
|
|
51
58
|
continue;
|
|
@@ -62,8 +69,28 @@ const globToRegex = (pattern: string): RegExp => {
|
|
|
62
69
|
return new RegExp(`^${output}$`);
|
|
63
70
|
};
|
|
64
71
|
|
|
72
|
+
// Fallback for wildcard-free patterns (e.g. `generated`, `shared-kernel`). It
|
|
73
|
+
// must align on a `/` segment boundary at the START and terminate on a real
|
|
74
|
+
// boundary — a `/`, the extension dot, or end of string. This replaces a naive
|
|
75
|
+
// `value.includes(pattern stripped of * and /)` substring test that over-matched
|
|
76
|
+
// (e.g. `vendor` matched `domain/vendorized-service.ts`), which silently
|
|
77
|
+
// suppressed genuine layer violations. The boundary-terminated form keeps
|
|
78
|
+
// intended ignores working (`generated` → `generated.ts`) while no longer
|
|
79
|
+
// swallowing unrelated segments.
|
|
80
|
+
const segmentPrefixRegex = (pattern: string): RegExp => {
|
|
81
|
+
return new RegExp(`(^|/)${escapeRegex(pattern)}([./]|$)`);
|
|
82
|
+
};
|
|
83
|
+
|
|
65
84
|
export const matchesPattern = (value: string, pattern: string): boolean => {
|
|
66
|
-
|
|
85
|
+
if (globToRegex(pattern).test(value)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
// Wildcard patterns are authoritative via the glob regex above; only bare
|
|
89
|
+
// (wildcard-free) patterns fall back to segment-boundary matching.
|
|
90
|
+
if (pattern.includes('*')) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return segmentPrefixRegex(pattern).test(value);
|
|
67
94
|
};
|
|
68
95
|
|
|
69
96
|
const canonicalCycleKey = (path: readonly string[]): string => {
|
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
CHANGED
|
@@ -48,6 +48,7 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
|
|
|
48
48
|
enabled: resolvedConfig.layers.L3.enabled,
|
|
49
49
|
validators: l3Validators.length > 0 ? l3Validators : ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
|
|
50
50
|
coverageThreshold: resolvedConfig.layers.L3.coverageThreshold,
|
|
51
|
+
requirementMatrixPath: resolvedConfig.layers.L3.requirementMatrixPath,
|
|
51
52
|
},
|
|
52
53
|
L4: {
|
|
53
54
|
enabled: resolvedConfig.layers.L4.enabled,
|
|
@@ -10,12 +10,14 @@ interface L3ConfigProps {
|
|
|
10
10
|
readonly enabled: boolean;
|
|
11
11
|
readonly validators: string[];
|
|
12
12
|
readonly coverageThreshold: number;
|
|
13
|
+
readonly requirementMatrixPath?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export class L3Config {
|
|
16
17
|
readonly enabled: boolean;
|
|
17
18
|
readonly validators: readonly string[];
|
|
18
19
|
readonly coverageThreshold: number;
|
|
20
|
+
readonly requirementMatrixPath?: string;
|
|
19
21
|
|
|
20
22
|
constructor(props: L3ConfigProps) {
|
|
21
23
|
if (props.coverageThreshold < 0) {
|
|
@@ -37,6 +39,7 @@ export class L3Config {
|
|
|
37
39
|
this.enabled = props.enabled;
|
|
38
40
|
this.validators = Object.freeze([...props.validators]);
|
|
39
41
|
this.coverageThreshold = props.coverageThreshold;
|
|
42
|
+
this.requirementMatrixPath = props.requirementMatrixPath;
|
|
40
43
|
Object.freeze(this);
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -44,6 +47,7 @@ export class L3Config {
|
|
|
44
47
|
enabled: boolean;
|
|
45
48
|
validators: string[];
|
|
46
49
|
coverageThreshold: number;
|
|
50
|
+
requirementMatrixPath?: string;
|
|
47
51
|
}): L3Config {
|
|
48
52
|
return new L3Config(raw);
|
|
49
53
|
}
|
|
@@ -13,7 +13,7 @@ import { L4Config } from './l4-config.js';
|
|
|
13
13
|
interface ResolvedLayersDocument {
|
|
14
14
|
L1: { enabled: boolean; rules: Record<string, string> };
|
|
15
15
|
L2: { enabled: boolean; validators: string[] };
|
|
16
|
-
L3: { enabled: boolean; validators: string[]; coverageThreshold: number };
|
|
16
|
+
L3: { enabled: boolean; validators: string[]; coverageThreshold: number; requirementMatrixPath?: string };
|
|
17
17
|
L4: { enabled: boolean; validators: string[]; schedule: string };
|
|
18
18
|
}
|
|
19
19
|
|
package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json
CHANGED
|
@@ -126,6 +126,10 @@
|
|
|
126
126
|
"type": "number",
|
|
127
127
|
"minimum": 0,
|
|
128
128
|
"maximum": 100
|
|
129
|
+
},
|
|
130
|
+
"requirementMatrixPath": {
|
|
131
|
+
"type": "string",
|
|
132
|
+
"minLength": 1
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
},
|
|
@@ -344,20 +348,21 @@
|
|
|
344
348
|
"type": "object",
|
|
345
349
|
"additionalProperties": false,
|
|
346
350
|
"required": [
|
|
347
|
-
"
|
|
348
|
-
"
|
|
351
|
+
"inception",
|
|
352
|
+
"product",
|
|
353
|
+
"required"
|
|
349
354
|
],
|
|
350
355
|
"properties": {
|
|
351
|
-
"
|
|
356
|
+
"inception": {
|
|
352
357
|
"type": "string",
|
|
353
358
|
"minLength": 1
|
|
354
359
|
},
|
|
355
|
-
"
|
|
356
|
-
"type": "
|
|
357
|
-
"
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
360
|
+
"product": {
|
|
361
|
+
"type": "string",
|
|
362
|
+
"minLength": 1
|
|
363
|
+
},
|
|
364
|
+
"required": {
|
|
365
|
+
"type": "boolean"
|
|
361
366
|
}
|
|
362
367
|
}
|
|
363
368
|
}
|
package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json
CHANGED
|
@@ -136,6 +136,10 @@
|
|
|
136
136
|
"type": "number",
|
|
137
137
|
"minimum": 0,
|
|
138
138
|
"maximum": 100
|
|
139
|
+
},
|
|
140
|
+
"requirementMatrixPath": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"minLength": 1
|
|
139
143
|
}
|
|
140
144
|
}
|
|
141
145
|
},
|
|
@@ -354,20 +358,21 @@
|
|
|
354
358
|
"type": "object",
|
|
355
359
|
"additionalProperties": false,
|
|
356
360
|
"required": [
|
|
357
|
-
"
|
|
358
|
-
"
|
|
361
|
+
"inception",
|
|
362
|
+
"product",
|
|
363
|
+
"required"
|
|
359
364
|
],
|
|
360
365
|
"properties": {
|
|
361
|
-
"
|
|
366
|
+
"inception": {
|
|
362
367
|
"type": "string",
|
|
363
368
|
"minLength": 1
|
|
364
369
|
},
|
|
365
|
-
"
|
|
366
|
-
"type": "
|
|
367
|
-
"
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
370
|
+
"product": {
|
|
371
|
+
"type": "string",
|
|
372
|
+
"minLength": 1
|
|
373
|
+
},
|
|
374
|
+
"required": {
|
|
375
|
+
"type": "boolean"
|
|
371
376
|
}
|
|
372
377
|
}
|
|
373
378
|
}
|
package/scripts/harness/harness-api/infrastructure/adapters/phase-dependency-model-query-adapter.ts
CHANGED
|
@@ -61,8 +61,15 @@ export class PhaseDependencyModelQueryAdapter implements PhaseGateQueryPort {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
return results;
|
|
64
|
-
} catch {
|
|
65
|
-
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// Fail-closed: returning [] here would make CheckReadyResult.fromStories treat
|
|
66
|
+
// "no stories" as an unconditional pass. Surface the error so the dispatcher
|
|
67
|
+
// reports a non-passing (error) result instead of silently opening the gate.
|
|
68
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
69
|
+
console.error(`[harness-api] queryAllStories failed to evaluate phase gates: ${message}`);
|
|
70
|
+
throw err instanceof Error
|
|
71
|
+
? err
|
|
72
|
+
: new Error(`queryAllStories failed to evaluate phase gates: ${message}`);
|
|
66
73
|
}
|
|
67
74
|
},
|
|
68
75
|
|
|
@@ -91,8 +98,15 @@ export class PhaseDependencyModelQueryAdapter implements PhaseGateQueryPort {
|
|
|
91
98
|
currentPhase: `PHASE-${result.targetLevel}`,
|
|
92
99
|
completedGates: result.passed ? [`level-${result.targetLevel}`] : [],
|
|
93
100
|
});
|
|
94
|
-
} catch {
|
|
95
|
-
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// Fail-closed: returning null here conflates a genuine execution error with
|
|
103
|
+
// "unit not found" and hides the failure. Surface the error so the dispatcher
|
|
104
|
+
// reports a non-passing (error) result instead of masking it.
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
console.error(`[harness-api] queryUnit failed to evaluate phase gate for '${unitId}': ${message}`);
|
|
107
|
+
throw err instanceof Error
|
|
108
|
+
? err
|
|
109
|
+
: new Error(`queryUnit failed to evaluate phase gate for '${unitId}': ${message}`);
|
|
96
110
|
}
|
|
97
111
|
},
|
|
98
112
|
};
|
package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts
CHANGED
|
@@ -10,6 +10,19 @@ import type { DriftItem } from '../../domain/value-objects/drift-report-summary.
|
|
|
10
10
|
import { toValidatorSystemConfig } from '../../../config-foundation/application/mappers/validator-system-config-mapper.js';
|
|
11
11
|
import { ConfigNotFoundError } from '../../../config-foundation/infrastructure/repositories/file-system-config-repository.js';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* validator-system config(toValidatorSystemConfig の出力)から
|
|
15
|
+
* L3-004 用の requirement-test-matrix.json パスを取り出す。
|
|
16
|
+
* 未設定・config 不在時は undefined を返し、下流の AC coverage adapter が
|
|
17
|
+
* 既定値(.harness/requirement-test-matrix.json)へフォールバックする。
|
|
18
|
+
*/
|
|
19
|
+
function extractRequirementMatrixPath(config: object | undefined): string | undefined {
|
|
20
|
+
if (!config) return undefined;
|
|
21
|
+
const layers = (config as { layers?: { L3?: { requirementMatrixPath?: unknown } } }).layers;
|
|
22
|
+
const path = layers?.L3?.requirementMatrixPath;
|
|
23
|
+
return typeof path === 'string' && path.length > 0 ? path : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
// Override interface preserved for testing
|
|
14
27
|
export interface IValidatorSystemStub {
|
|
15
28
|
runL3Validators(): Promise<ValidatorCheckItem[]>;
|
|
@@ -40,8 +53,12 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
|
|
|
40
53
|
return {
|
|
41
54
|
async runL3Validators(): Promise<ValidatorCheckItem[]> {
|
|
42
55
|
const { createValidatorSystemModule } = await import('../../../validator-system/composition-root.js');
|
|
43
|
-
const
|
|
44
|
-
const
|
|
56
|
+
const config = await loadValidatorSystemConfig();
|
|
57
|
+
const mod = createValidatorSystemModule(config);
|
|
58
|
+
const results = await mod.runL3ValidatorsUseCase.execute({
|
|
59
|
+
targetPaths: [],
|
|
60
|
+
requirementMatrixPath: extractRequirementMatrixPath(config),
|
|
61
|
+
});
|
|
45
62
|
return results.map((r) => ({
|
|
46
63
|
validatorId: r.validatorId,
|
|
47
64
|
passed: r.passed,
|
|
@@ -52,8 +69,14 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
|
|
|
52
69
|
|
|
53
70
|
async runAllValidators(): Promise<ValidatorCheckItem[]> {
|
|
54
71
|
const { createValidatorSystemModule } = await import('../../../validator-system/composition-root.js');
|
|
55
|
-
const
|
|
56
|
-
const
|
|
72
|
+
const config = await loadValidatorSystemConfig();
|
|
73
|
+
const mod = createValidatorSystemModule(config);
|
|
74
|
+
const report = await mod.runFullValidationUseCase.execute({
|
|
75
|
+
targetPaths: [],
|
|
76
|
+
unitName: '',
|
|
77
|
+
currentPhase: '',
|
|
78
|
+
requirementMatrixPath: extractRequirementMatrixPath(config),
|
|
79
|
+
});
|
|
57
80
|
return report.results.map((r) => ({
|
|
58
81
|
validatorId: r.validatorId,
|
|
59
82
|
passed: r.passed,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-219
|
|
4
|
+
|
|
5
|
+
export type ModelDelegationPolicy = "delegate-sonnet" | "none";
|
|
6
|
+
|
|
7
|
+
export interface ModelDelegationPort {
|
|
8
|
+
readPolicy(projectRoot: string): Promise<ModelDelegationPolicy>;
|
|
9
|
+
renderSkill(content: string, policy: ModelDelegationPolicy): string;
|
|
10
|
+
}
|