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.
Files changed (29) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/docs/guide/installation.md +1 -1
  3. package/docs/templates/ci/aidlc-gate.yml +22 -4
  4. package/package.json +2 -2
  5. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
  6. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  7. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  8. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  9. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
  10. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
  11. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  12. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  13. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
  14. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
  15. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  16. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
  17. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  18. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  19. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  20. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  21. package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
  22. package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
  23. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  24. package/scripts/harness/main.ts +26 -6
  25. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +24 -7
  26. package/scripts/harness/validator-system/composition-root.ts +4 -1
  27. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  28. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
  29. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
@@ -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
- async function loadResolvedConfig(): Promise<HarnessConfigV2 | undefined> {
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
- // - coverageThreshold 設定あり → getCoverage() try/catch で包み FAIL-CLOSED で判定する
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: "vitest --coverage 等でカバレッジレポートを生成してから再実行してください",
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
- if (!policyResult.passed) {
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
- coverageThreshold: 90,
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 '../value-objects/validation-result.js';
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
- const languages = this.config.project?.languages;
99
- return languages && languages.length > 0 ? [...languages] : ["typescript"];
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 'node:fs/promises';
23
- import { isAbsolute, join } from 'node:path';
24
- import type { AcCoveragePolicyPort } from '../../domain/ports/ac-coverage-policy-port.js';
25
- import type { HarnessErrorLike } from '../../domain/value-objects/validation-result.js';
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 = '.harness/requirement-test-matrix.json';
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: 'L3-004', toString: () => 'L3-004' },
32
- severity: { value: 'error', toString: () => 'error' },
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
- ? context.matrixFilePath
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('../../../nyquist-validation/composition-root.js');
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
- 'requirement-test-matrix.json の存在・形式・スキーマ整合性を確認してください',
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('../../../config-foundation/composition-root.js');
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('../../../traceability-model/composition-root.js');
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
  });