phasegate 0.264.0 → 0.283.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 +38 -0
- package/docs/guide/installation.md +1 -1
- package/docs/templates/ci/aidlc-gate.yml +22 -4
- package/package.json +2 -2
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
- package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
- package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
- package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
- package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
- package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
- package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
- package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
- package/scripts/harness/main.ts +26 -6
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +24 -7
- package/scripts/harness/validator-system/composition-root.ts +4 -1
- package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
package/scripts/harness/main.ts
CHANGED
|
@@ -694,8 +694,8 @@ Options:
|
|
|
694
694
|
--agent <claude|codex|both> Agent context and hook targets (default: both)
|
|
695
695
|
--skills <core|all> Rendered agent context skill mode (default: all)
|
|
696
696
|
--workflow <standard|strict> Rendered agent context workflow mode (default: standard)
|
|
697
|
-
--with-husky Include Husky hook targets
|
|
698
|
-
--with-ci Include GitHub Actions target
|
|
697
|
+
--with-husky Include Husky hook targets (opt-in; omitted by default)
|
|
698
|
+
--with-ci Include GitHub Actions target (opt-in; omitted by default)
|
|
699
699
|
--personal Use local-only install: no package.json, agent docs, Husky, CI, .gitignore, GitHub CLI, secrets, or CI setting writes.
|
|
700
700
|
With --agent claude, initializes .phasegate-local config/settings/skills and ignored .claude shims.
|
|
701
701
|
--json Output machine-readable JSON
|
|
@@ -1855,7 +1855,18 @@ function emitV2SchemaWarningOnce(sourcePath: string): void {
|
|
|
1855
1855
|
);
|
|
1856
1856
|
}
|
|
1857
1857
|
|
|
1858
|
-
|
|
1858
|
+
/**
|
|
1859
|
+
* 不正 config でも fail-open(警告 + 既定設定で続行)にするコマンド。
|
|
1860
|
+
* GitHub #40: config 検証の fail-closed が dispatch より上流にあると、pre-tool-use hook
|
|
1861
|
+
* 経由で Write/Edit/Bash が全遮断され、config 自身を修復する経路が消える(自己修復
|
|
1862
|
+
* デッドロック)。hook(エージェントのツール遮断点)と doctor(自己診断)は復旧経路
|
|
1863
|
+
* として常に起動可能でなければならない。validate / ci-check 等の検査系コマンドは
|
|
1864
|
+
* fail-closed を維持する(gated スコープへの書き込みは hook 内の phase-gate 判定が
|
|
1865
|
+
* 引き続き fail-closed でブロックする)。
|
|
1866
|
+
*/
|
|
1867
|
+
const CONFIG_FAIL_OPEN_COMMANDS: ReadonlySet<string> = new Set(["hook", "doctor"]);
|
|
1868
|
+
|
|
1869
|
+
async function loadResolvedConfig(command?: string): Promise<HarnessConfigV2 | undefined> {
|
|
1859
1870
|
try {
|
|
1860
1871
|
const configModule = createConfigFoundationModule();
|
|
1861
1872
|
const result = await configModule.usecases.loadResolvedConfigUseCase.execute();
|
|
@@ -1866,6 +1877,15 @@ async function loadResolvedConfig(): Promise<HarnessConfigV2 | undefined> {
|
|
|
1866
1877
|
} catch (error) {
|
|
1867
1878
|
if (error instanceof ConfigValidationError) {
|
|
1868
1879
|
process.stderr.write(`Invalid phasegate.config.json: ${error.message}\n`);
|
|
1880
|
+
if (command !== undefined && CONFIG_FAIL_OPEN_COMMANDS.has(command)) {
|
|
1881
|
+
process.stderr.write(
|
|
1882
|
+
"Warning: continuing with default settings so diagnosis and self-repair stay possible. Fix the reported path in phasegate.config.json to restore full gating.\n",
|
|
1883
|
+
);
|
|
1884
|
+
return undefined;
|
|
1885
|
+
}
|
|
1886
|
+
process.stderr.write(
|
|
1887
|
+
"Recovery: fix the reported path/type in phasegate.config.json (or restore it from version control). `phasegate doctor` and agent hooks remain available while the config is invalid.\n",
|
|
1888
|
+
);
|
|
1869
1889
|
process.exit(2);
|
|
1870
1890
|
}
|
|
1871
1891
|
if (error instanceof ConfigNotFoundError) {
|
|
@@ -1942,7 +1962,7 @@ async function main(): Promise<void> {
|
|
|
1942
1962
|
const json = hasFlag(args, "--json");
|
|
1943
1963
|
|
|
1944
1964
|
// Cross-unit wiring: 設定を先に解決し、各Unit に注入する
|
|
1945
|
-
const resolvedConfig = command.startsWith("world:") ? undefined : await loadResolvedConfig();
|
|
1965
|
+
const resolvedConfig = command.startsWith("world:") ? undefined : await loadResolvedConfig(command);
|
|
1946
1966
|
|
|
1947
1967
|
try {
|
|
1948
1968
|
switch (command) {
|
|
@@ -2346,8 +2366,8 @@ async function main(): Promise<void> {
|
|
|
2346
2366
|
force: hasFlag(args, "--force"),
|
|
2347
2367
|
includeClaude,
|
|
2348
2368
|
includeCodex,
|
|
2349
|
-
includeHusky: !personal,
|
|
2350
|
-
includeCi: !personal,
|
|
2369
|
+
includeHusky: !personal && hasFlag(args, "--with-husky"),
|
|
2370
|
+
includeCi: !personal && hasFlag(args, "--with-ci"),
|
|
2351
2371
|
skillSet: skillSetRaw,
|
|
2352
2372
|
workflow: parseWorkflowMode(workflowRaw),
|
|
2353
2373
|
agent,
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* @unit validator-system
|
|
4
4
|
* @work-item-id WI-212
|
|
5
5
|
* @work-item-id WI-302
|
|
6
|
+
* @work-item-id WI-317
|
|
7
|
+
* @work-item-id WI-324
|
|
6
8
|
*
|
|
7
9
|
* RunL3ValidatorsUseCase — H08-02: L3バリデータ実行
|
|
8
10
|
*/
|
|
@@ -22,8 +24,8 @@ import {
|
|
|
22
24
|
type ValidatorExecutionService,
|
|
23
25
|
} from "../../domain/services/validator-execution-service.js";
|
|
24
26
|
import { ValidatorLanguageCapabilityService } from "../../domain/services/validator-language-capability-service.js";
|
|
25
|
-
import { WorldConstraintRederivationService } from "../../domain/services/world-constraint-rederivation-service.js";
|
|
26
27
|
import type { ValidatorRegistry } from "../../domain/services/validator-registry.js";
|
|
28
|
+
import { WorldConstraintRederivationService } from "../../domain/services/world-constraint-rederivation-service.js";
|
|
27
29
|
import type { HarnessErrorLike } from "../../domain/value-objects/validation-result.js";
|
|
28
30
|
import { ValidationResult } from "../../domain/value-objects/validation-result.js";
|
|
29
31
|
import { ValidatorId } from "../../domain/value-objects/validator-id.js";
|
|
@@ -133,8 +135,10 @@ export class RunL3ValidatorsUseCase {
|
|
|
133
135
|
);
|
|
134
136
|
|
|
135
137
|
// L3-003: カバレッジ判定(カバレッジゲートはオプトイン)
|
|
136
|
-
// - coverageThreshold 未設定 → SKIP(透過的に判定をスキップ。getCoverage() は呼ばない)
|
|
137
|
-
// -
|
|
138
|
+
// - coverageThreshold 未設定 or 0 → SKIP(透過的に判定をスキップ。getCoverage() は呼ばない)
|
|
139
|
+
// 0 は正規の opt-out(ドメイン VO L3Config.hasCoverageGate() の「threshold > 0 でのみ有効」と整合。
|
|
140
|
+
// minimal preset の coverageThreshold: 0 もこの opt-out 意図。WI-317 / github#37)
|
|
141
|
+
// - coverageThreshold > 0 → getCoverage() を try/catch で包み FAIL-CLOSED で判定する
|
|
138
142
|
// - 閾値未満 → FAIL / 閾値以上 → PASS
|
|
139
143
|
// - レポート不在などで getCoverage() が失敗 → FAIL(合格扱いにしない)
|
|
140
144
|
// このブロックは例外を送出せず、L3-003 の per-validator 結果のみを差し替える。
|
|
@@ -145,12 +149,12 @@ export class RunL3ValidatorsUseCase {
|
|
|
145
149
|
const l3003Id = ValidatorId.create("L3-003");
|
|
146
150
|
const threshold = layerConfig.getThreshold("coverageThreshold");
|
|
147
151
|
|
|
148
|
-
if (threshold === null) {
|
|
152
|
+
if (threshold === null || threshold === 0) {
|
|
149
153
|
overrideMap.set(
|
|
150
154
|
"L3-003",
|
|
151
155
|
ValidationResult.skipWithReason(
|
|
152
156
|
l3003Id,
|
|
153
|
-
"coverageThreshold
|
|
157
|
+
"coverageThreshold が未設定/0 のためカバレッジ判定をスキップ(カバレッジゲートはオプトイン。0 で opt-out)",
|
|
154
158
|
),
|
|
155
159
|
);
|
|
156
160
|
} else {
|
|
@@ -186,7 +190,8 @@ export class RunL3ValidatorsUseCase {
|
|
|
186
190
|
code: "L3-003",
|
|
187
191
|
severity: "error",
|
|
188
192
|
message: `coverageThreshold=${threshold}% が設定されていますがカバレッジレポートが見つかりません(テストをカバレッジ付きで実行してください)`,
|
|
189
|
-
suggestion:
|
|
193
|
+
suggestion:
|
|
194
|
+
'次のいずれかで解消してください: (a) テストをカバレッジ付きで実行してレポートを生成する(例: vitest --coverage)、(b) カバレッジゲートを opt-out するなら config の layers.L3.coverageThreshold を 0 に設定する、(c) 非 JS/TS プロジェクトなら project.languages を宣言する(例: ["python"]。L3-003 自体が unsupported-language SKIP になる)',
|
|
190
195
|
},
|
|
191
196
|
],
|
|
192
197
|
0,
|
|
@@ -224,7 +229,19 @@ export class RunL3ValidatorsUseCase {
|
|
|
224
229
|
const policyResult = await this.acCoveragePolicyPort.checkCoverage({
|
|
225
230
|
matrixFilePath: input.requirementMatrixPath,
|
|
226
231
|
});
|
|
227
|
-
|
|
232
|
+
// WI-324: フレッシュプロジェクト(story 未作成・matrix 未生成)は policy adapter が
|
|
233
|
+
// skipped=true を返すので、L3-003 の opt-out と同じ表現で skipWithReason に変換する。
|
|
234
|
+
// story が存在するのに matrix が不在の場合は従来どおり fail-closed(下の分岐)。
|
|
235
|
+
if (policyResult.skipped) {
|
|
236
|
+
overrideMap.set(
|
|
237
|
+
"L3-004",
|
|
238
|
+
ValidationResult.skipWithReason(
|
|
239
|
+
ValidatorId.create("L3-004"),
|
|
240
|
+
policyResult.skipReason ??
|
|
241
|
+
"story 未作成のため L3-004 をスキップ(story 作成後に requirement-test-matrix を生成すると有効化されます)",
|
|
242
|
+
),
|
|
243
|
+
);
|
|
244
|
+
} else if (!policyResult.passed) {
|
|
228
245
|
overrideMap.set("L3-004", ValidationResult.fail(ValidatorId.create("L3-004"), [...policyResult.errors], 0));
|
|
229
246
|
}
|
|
230
247
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @work-item-id WI-301
|
|
9
9
|
* @work-item-id WI-302
|
|
10
10
|
* @work-item-id WI-305
|
|
11
|
+
* @work-item-id WI-322
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { join } from "node:path";
|
|
@@ -73,7 +74,9 @@ const DEFAULT_CONFIG = {
|
|
|
73
74
|
L3: {
|
|
74
75
|
enabled: true,
|
|
75
76
|
validators: ["L3-001", "L3-002", "L3-003", "L3-004", "L3-006", "L3-007"],
|
|
76
|
-
|
|
77
|
+
// WI-322 (github#37 残課題): カバレッジゲートはオプトイン(WI-317)。config なし環境の
|
|
78
|
+
// fallback で 90% を強制しない。0 = 正規の opt-out(L3-003 は透過 SKIP になる)。
|
|
79
|
+
coverageThreshold: 0,
|
|
77
80
|
bundleSizeLimit: 512000,
|
|
78
81
|
requirementMatrixPath: ".harness/requirement-test-matrix.json",
|
|
79
82
|
},
|
|
@@ -4,11 +4,20 @@
|
|
|
4
4
|
*
|
|
5
5
|
* AcCoveragePolicyPort — nyquist-validation AcCoverageGatePolicy(L3-004)
|
|
6
6
|
*/
|
|
7
|
-
import type { HarnessErrorLike } from
|
|
7
|
+
import type { HarnessErrorLike } from "../value-objects/validation-result.js";
|
|
8
8
|
|
|
9
9
|
export interface AcCoveragePolicyPort {
|
|
10
10
|
checkCoverage(context: { matrixFilePath?: string }): Promise<{
|
|
11
11
|
passed: boolean;
|
|
12
12
|
errors: readonly HarnessErrorLike[];
|
|
13
|
+
/**
|
|
14
|
+
* WI-324: フレッシュプロジェクト(story 未作成・matrix 未生成)では L3-004 を
|
|
15
|
+
* fail-closed ではなく SKIP として扱う。true の場合、上位(RunL3ValidatorsUseCase)は
|
|
16
|
+
* skipReason 付きの skipWithReason 結果へ変換する。省略時(既存実装・既存モック)は
|
|
17
|
+
* 従来どおり passed/errors のみで判定される(後方互換 optional)。
|
|
18
|
+
*/
|
|
19
|
+
skipped?: boolean;
|
|
20
|
+
/** skipped=true のときの人間可読なスキップ理由。 */
|
|
21
|
+
skipReason?: string;
|
|
13
22
|
}>;
|
|
14
23
|
}
|
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
* @work-item-id WI-212
|
|
6
6
|
* @work-item-id WI-301
|
|
7
7
|
* @work-item-id WI-302
|
|
8
|
+
* @work-item-id WI-319
|
|
9
|
+
* @work-item-id WI-328
|
|
8
10
|
*
|
|
9
11
|
* HarnessConfigValidatorConfigAdapter — ValidatorConfigPort実装
|
|
10
12
|
* HarnessConfigV2からLayerConfig VOを構築する
|
|
11
13
|
*/
|
|
12
14
|
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
13
17
|
import type { ValidatorConfigPort } from "../../domain/ports/validator-config-port.js";
|
|
14
18
|
import { LayerConfig } from "../../domain/value-objects/layer-config.js";
|
|
15
19
|
import { ValidatorId } from "../../domain/value-objects/validator-id.js";
|
|
@@ -38,11 +42,92 @@ export interface HarnessConfigV2Like {
|
|
|
38
42
|
world?: { enabled?: boolean };
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
/**
|
|
46
|
+
* WI-319 (github#39): project.languages 未宣言時にファイルシステムから言語を検出するためのマーカー定義。
|
|
47
|
+
* typescript は package.json の存在自体を根拠にできない(phasegate 導入時に phasegate 用
|
|
48
|
+
* package.json が置かれるため)ので、この表には含めず hasTypescriptMarker() で個別判定する。
|
|
49
|
+
*/
|
|
50
|
+
const LANGUAGE_MARKER_FILES: ReadonlyArray<{ readonly language: string; readonly markers: readonly string[] }> = [
|
|
51
|
+
{ language: "python", markers: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt"] },
|
|
52
|
+
{ language: "go", markers: ["go.mod"] },
|
|
53
|
+
{ language: "rust", markers: ["Cargo.toml"] },
|
|
54
|
+
{ language: "java", markers: ["pom.xml", "build.gradle", "build.gradle.kts"] },
|
|
55
|
+
{ language: "ruby", markers: ["Gemfile"] },
|
|
56
|
+
{ language: "php", markers: ["composer.json"] },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** WI-328: 実効言語リストの出所。status 表示等で「なぜこの言語判定になったか」を示す。 */
|
|
60
|
+
export type ProjectLanguageSource = "declared" | "detected" | "fallback";
|
|
61
|
+
|
|
62
|
+
export interface ResolvedProjectLanguages {
|
|
63
|
+
readonly languages: readonly string[];
|
|
64
|
+
readonly source: ProjectLanguageSource;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* WI-328 (github#39 残課題): 実効言語リストとその出所を解決する唯一の実装。
|
|
69
|
+
* getProjectLanguages()(WI-319 の検出ロジック)と phasegate:status の言語表示が
|
|
70
|
+
* 同じテーブル・同じ優先順位を共有するために export する。
|
|
71
|
+
*
|
|
72
|
+
* 優先順位:
|
|
73
|
+
* 1. declared — config の project.languages 宣言(従来どおり最優先)
|
|
74
|
+
* 2. detected — project root のマーカーファイルから検出(WI-319 / github#39)
|
|
75
|
+
* 3. fallback — 検出ゼロなら typescript フォールバック(純 JS リポジトリ等の挙動維持)
|
|
76
|
+
*/
|
|
77
|
+
export function resolveProjectLanguages(
|
|
78
|
+
declaredLanguages: readonly string[] | undefined,
|
|
79
|
+
rootDir: string,
|
|
80
|
+
): ResolvedProjectLanguages {
|
|
81
|
+
if (declaredLanguages && declaredLanguages.length > 0) {
|
|
82
|
+
return { languages: [...declaredLanguages], source: "declared" };
|
|
83
|
+
}
|
|
84
|
+
const detected = detectLanguagesFromFilesystem(rootDir);
|
|
85
|
+
if (detected.length > 0) {
|
|
86
|
+
return { languages: detected, source: "detected" };
|
|
87
|
+
}
|
|
88
|
+
return { languages: ["typescript"], source: "fallback" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function detectLanguagesFromFilesystem(rootDir: string): string[] {
|
|
92
|
+
const detected: string[] = [];
|
|
93
|
+
if (hasTypescriptMarker(rootDir)) detected.push("typescript");
|
|
94
|
+
for (const { language, markers } of LANGUAGE_MARKER_FILES) {
|
|
95
|
+
if (markers.some((marker) => existsSync(join(rootDir, marker)))) {
|
|
96
|
+
detected.push(language);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return detected;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* typescript の根拠は tsconfig.json の存在、または package.json の
|
|
104
|
+
* dependencies / devDependencies に typescript があることのみ。
|
|
105
|
+
* package.json の存在自体は根拠にしない(phasegate 導入時に phasegate 用
|
|
106
|
+
* package.json が置かれるため、誤検出の原因になる)。
|
|
107
|
+
*/
|
|
108
|
+
function hasTypescriptMarker(rootDir: string): boolean {
|
|
109
|
+
if (existsSync(join(rootDir, "tsconfig.json"))) return true;
|
|
110
|
+
const packageJsonPath = join(rootDir, "package.json");
|
|
111
|
+
if (!existsSync(packageJsonPath)) return false;
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
|
|
114
|
+
dependencies?: Record<string, string>;
|
|
115
|
+
devDependencies?: Record<string, string>;
|
|
116
|
+
};
|
|
117
|
+
return parsed.dependencies?.typescript !== undefined || parsed.devDependencies?.typescript !== undefined;
|
|
118
|
+
} catch {
|
|
119
|
+
// parse 失敗は typescript 根拠なしとして扱う(throw しない)
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
41
124
|
export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort {
|
|
42
125
|
private readonly config: HarnessConfigV2Like;
|
|
126
|
+
private readonly rootDir: string;
|
|
43
127
|
|
|
44
|
-
constructor(config: HarnessConfigV2Like) {
|
|
128
|
+
constructor(config: HarnessConfigV2Like, rootDir: string = process.cwd()) {
|
|
45
129
|
this.config = config;
|
|
130
|
+
this.rootDir = rootDir;
|
|
46
131
|
}
|
|
47
132
|
|
|
48
133
|
async getLayerConfig(layer: "L2" | "L3" | "L4"): Promise<LayerConfig> {
|
|
@@ -95,8 +180,9 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
|
|
|
95
180
|
}
|
|
96
181
|
|
|
97
182
|
async getProjectLanguages(): Promise<readonly string[]> {
|
|
98
|
-
|
|
99
|
-
|
|
183
|
+
// WI-319 の宣言優先 → FS 検出 → typescript フォールバックの解決は
|
|
184
|
+
// resolveProjectLanguages() に一本化(WI-328 で status 表示と共有)。
|
|
185
|
+
return [...resolveProjectLanguages(this.config.project?.languages, this.rootDir).languages];
|
|
100
186
|
}
|
|
101
187
|
|
|
102
188
|
private normalizeValidatorId(idOrName: string): string {
|
|
@@ -18,18 +18,25 @@
|
|
|
18
18
|
* (layers.L3.requirementMatrixPath)から供給される。未指定時は既定値
|
|
19
19
|
* `.harness/requirement-test-matrix.json` を用いる。マトリクスが不在の場合は
|
|
20
20
|
* fail-closed で「設定されているが不在」という実行可能なメッセージを返す。
|
|
21
|
+
*
|
|
22
|
+
* FRESH PROJECT SKIP (WI-324): 例外として「matrix 不在 かつ StoryCatalog の
|
|
23
|
+
* story がゼロ」の場合のみ skipped=true を返す。phasegate 導入直後(story 未作成・
|
|
24
|
+
* matrix 未生成)のオンボーディングを fail-closed で阻害しないため。story が
|
|
25
|
+
* 1件でも存在するのに matrix が不在なら「あるべき matrix が消えた事故」として
|
|
26
|
+
* 従来どおり fail-closed。story 数の取得自体に失敗した場合も判定不能として
|
|
27
|
+
* 保守的に fail-closed 側へ倒す。
|
|
21
28
|
*/
|
|
22
|
-
import { access } from
|
|
23
|
-
import { isAbsolute, join } from
|
|
24
|
-
import type { AcCoveragePolicyPort } from
|
|
25
|
-
import type { HarnessErrorLike } from
|
|
29
|
+
import { access } from "node:fs/promises";
|
|
30
|
+
import { isAbsolute, join } from "node:path";
|
|
31
|
+
import type { AcCoveragePolicyPort } from "../../domain/ports/ac-coverage-policy-port.js";
|
|
32
|
+
import type { HarnessErrorLike } from "../../domain/value-objects/validation-result.js";
|
|
26
33
|
|
|
27
|
-
const DEFAULT_MATRIX_PATH =
|
|
34
|
+
const DEFAULT_MATRIX_PATH = ".harness/requirement-test-matrix.json";
|
|
28
35
|
|
|
29
36
|
function toL3004Error(message: string, suggestion: string): HarnessErrorLike {
|
|
30
37
|
return {
|
|
31
|
-
code: { value:
|
|
32
|
-
severity: { value:
|
|
38
|
+
code: { value: "L3-004", toString: () => "L3-004" },
|
|
39
|
+
severity: { value: "error", toString: () => "error" },
|
|
33
40
|
message,
|
|
34
41
|
suggestion,
|
|
35
42
|
};
|
|
@@ -39,21 +46,29 @@ export class NyquistAcCoveragePolicyAdapter implements AcCoveragePolicyPort {
|
|
|
39
46
|
async checkCoverage(context: { matrixFilePath?: string }): Promise<{
|
|
40
47
|
passed: boolean;
|
|
41
48
|
errors: readonly HarnessErrorLike[];
|
|
49
|
+
skipped?: boolean;
|
|
50
|
+
skipReason?: string;
|
|
42
51
|
}> {
|
|
43
52
|
const rootDir = process.cwd();
|
|
44
53
|
const relativeOrAbsolute =
|
|
45
|
-
context.matrixFilePath && context.matrixFilePath.length > 0
|
|
46
|
-
|
|
47
|
-
: DEFAULT_MATRIX_PATH;
|
|
48
|
-
const matrixFilePath = isAbsolute(relativeOrAbsolute)
|
|
49
|
-
? relativeOrAbsolute
|
|
50
|
-
: join(rootDir, relativeOrAbsolute);
|
|
54
|
+
context.matrixFilePath && context.matrixFilePath.length > 0 ? context.matrixFilePath : DEFAULT_MATRIX_PATH;
|
|
55
|
+
const matrixFilePath = isAbsolute(relativeOrAbsolute) ? relativeOrAbsolute : join(rootDir, relativeOrAbsolute);
|
|
51
56
|
|
|
52
57
|
// マトリクス不在は threshold-configured-but-absent と同様に fail-closed で
|
|
53
58
|
// 明確なメッセージを返す(ENOENT の生メッセージで落とさない)。
|
|
59
|
+
// 例外(WI-324): story がゼロのフレッシュプロジェクトのみ SKIP を返す。
|
|
54
60
|
try {
|
|
55
61
|
await access(matrixFilePath);
|
|
56
62
|
} catch {
|
|
63
|
+
if (await this.hasZeroStories(rootDir)) {
|
|
64
|
+
return {
|
|
65
|
+
passed: true,
|
|
66
|
+
errors: [],
|
|
67
|
+
skipped: true,
|
|
68
|
+
skipReason:
|
|
69
|
+
"story 未作成のため L3-004 をスキップ(story 作成後に requirement-test-matrix を生成すると有効化されます)",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
57
72
|
return {
|
|
58
73
|
passed: false,
|
|
59
74
|
errors: [
|
|
@@ -69,7 +84,7 @@ export class NyquistAcCoveragePolicyAdapter implements AcCoveragePolicyPort {
|
|
|
69
84
|
// 有効 storyId 一覧を traceability-model の StoryCatalog から取得する(REAL registry)
|
|
70
85
|
const storyIds = await this.loadValidStoryIds(rootDir);
|
|
71
86
|
|
|
72
|
-
const { createNyquistValidationModule } = await import(
|
|
87
|
+
const { createNyquistValidationModule } = await import("../../../nyquist-validation/composition-root.js");
|
|
73
88
|
const mod = createNyquistValidationModule({
|
|
74
89
|
getStoryIds: async () => storyIds,
|
|
75
90
|
});
|
|
@@ -79,9 +94,7 @@ export class NyquistAcCoveragePolicyAdapter implements AcCoveragePolicyPort {
|
|
|
79
94
|
|
|
80
95
|
return {
|
|
81
96
|
passed: output.passed,
|
|
82
|
-
errors: output.errors.map((err) =>
|
|
83
|
-
toL3004Error(err.message, ''),
|
|
84
|
-
),
|
|
97
|
+
errors: output.errors.map((err) => toL3004Error(err.message, "")),
|
|
85
98
|
};
|
|
86
99
|
} catch (error) {
|
|
87
100
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -91,19 +104,35 @@ export class NyquistAcCoveragePolicyAdapter implements AcCoveragePolicyPort {
|
|
|
91
104
|
errors: [
|
|
92
105
|
toL3004Error(
|
|
93
106
|
`AC網羅ゲートの検査に失敗しました(fail-closed): ${message}`,
|
|
94
|
-
|
|
107
|
+
"requirement-test-matrix.json の存在・形式・スキーマ整合性を確認してください",
|
|
95
108
|
),
|
|
96
109
|
],
|
|
97
110
|
};
|
|
98
111
|
}
|
|
99
112
|
}
|
|
100
113
|
|
|
114
|
+
/**
|
|
115
|
+
* WI-324: 「story がゼロ」判定。StoryCatalog(traceability-model)の登録 story 数を
|
|
116
|
+
* シグナルとする。StoryCatalog は requirement-test-matrix の生成元と同じ
|
|
117
|
+
* user_stories.md を読むため、「matrix が本来存在しうるか」と一貫した判定になる。
|
|
118
|
+
* catalog 読み込み自体が失敗した場合(config 不正等)は判定不能として false を返し、
|
|
119
|
+
* 呼び出し側を保守的に fail-closed へ倒す。
|
|
120
|
+
*/
|
|
121
|
+
private async hasZeroStories(rootDir: string): Promise<boolean> {
|
|
122
|
+
try {
|
|
123
|
+
const storyIds = await this.loadValidStoryIds(rootDir);
|
|
124
|
+
return storyIds.length === 0;
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
101
130
|
private async loadValidStoryIds(rootDir: string): Promise<readonly string[]> {
|
|
102
|
-
const { createConfigFoundationModule } = await import(
|
|
131
|
+
const { createConfigFoundationModule } = await import("../../../config-foundation/composition-root.js");
|
|
103
132
|
const configModule = createConfigFoundationModule();
|
|
104
133
|
const resolvedConfig = await configModule.usecases.loadResolvedConfigUseCase.execute();
|
|
105
134
|
|
|
106
|
-
const { createTraceabilityModelModule } = await import(
|
|
135
|
+
const { createTraceabilityModelModule } = await import("../../../traceability-model/composition-root.js");
|
|
107
136
|
const traceModule = createTraceabilityModelModule(rootDir, {
|
|
108
137
|
pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs },
|
|
109
138
|
});
|