phasegate 0.283.0 → 0.315.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 (71) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/docs/ADR/017-warning-severity-aggregation.md +17 -0
  3. package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
  4. package/docs/guide/layer-model.md +2 -0
  5. package/docs/guide/quick-vs-full-mode.md +28 -3
  6. package/docs/guide/troubleshooting.md +37 -0
  7. package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
  8. package/package.json +1 -1
  9. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
  10. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
  11. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
  12. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +18 -3
  13. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
  14. package/scripts/harness/ci-governance/composition-root.ts +2 -2
  15. package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
  16. package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
  17. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  18. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  19. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  20. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -3
  21. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
  22. package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
  23. package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
  24. package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
  25. package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
  26. package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
  27. package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
  28. package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
  29. package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
  30. package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
  31. package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
  32. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
  33. package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
  34. package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
  35. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
  36. package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
  37. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
  38. package/scripts/harness/installation/application/usecases/run-install.ts +44 -11
  39. package/scripts/harness/installation/application/usecases/run-reconcile.ts +44 -11
  40. package/scripts/harness/installation/composition-root.ts +13 -3
  41. package/scripts/harness/installation/domain/check-id.ts +2 -0
  42. package/scripts/harness/installation/domain/config-status.ts +17 -0
  43. package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
  44. package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
  45. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
  46. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
  47. package/scripts/harness/integrations/pre-commit.ts +17 -3
  48. package/scripts/harness/main.ts +57 -9
  49. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
  50. package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
  51. package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
  52. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
  53. package/scripts/harness/quick-mode/composition-root.ts +25 -15
  54. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +72 -3
  55. package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
  56. package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
  57. package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
  58. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
  59. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
  60. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
  61. package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
  62. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +8 -0
  63. package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
  64. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
  65. package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
  66. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
  67. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
  68. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
  69. package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
  70. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
  71. package/skills/quick-implementor/SKILL.md +19 -0
@@ -6,10 +6,15 @@
6
6
  * paths から変更カテゴリを分類し fullModeRequired 判定と理由を返す UseCase
7
7
  */
8
8
 
9
- import { ChangedFile } from '../../domain/value-objects/changed-file.js';
10
- import { QuickModeJudgmentEngine } from '../../domain/services/quick-mode-judgment-engine.js';
11
- import type { QuickModeConfigPort } from '../ports/quick-mode-config-port.js';
12
- import type { ChangeCategoryClassificationContract, ChangeCategoryPerFile } from '../dto/change-category-classification-contract.js';
9
+ import { QuickModeJudgmentEngine } from "../../domain/services/quick-mode-judgment-engine.js";
10
+ import type { ChangeKind } from "../../domain/types/change-kind.js";
11
+ import { ChangedFile } from "../../domain/value-objects/changed-file.js";
12
+ import type {
13
+ ChangeCategoryClassificationContract,
14
+ ChangeCategoryPerFile,
15
+ } from "../dto/change-category-classification-contract.js";
16
+ import type { FileExistencePort } from "../ports/file-existence-port.js";
17
+ import type { QuickModeConfigPort } from "../ports/quick-mode-config-port.js";
13
18
 
14
19
  export interface ClassifyChangeCategoryUseCaseInput {
15
20
  readonly paths: readonly string[];
@@ -23,20 +28,41 @@ export interface ClassifyChangeCategoryUseCaseInput {
23
28
  export interface ClassifyChangeCategoryUseCaseDeps {
24
29
  quickModeConfigPort: QuickModeConfigPort;
25
30
  judgmentEngine?: QuickModeJudgmentEngine;
31
+ fileExistencePort?: FileExistencePort;
26
32
  }
27
33
 
28
34
  export class ClassifyChangeCategoryUseCase {
29
35
  private readonly quickModeConfigPort: QuickModeConfigPort;
30
36
  private readonly judgmentEngine: QuickModeJudgmentEngine;
37
+ private readonly fileExistencePort?: FileExistencePort;
31
38
 
32
39
  constructor(deps: ClassifyChangeCategoryUseCaseDeps) {
33
40
  this.quickModeConfigPort = deps.quickModeConfigPort;
34
41
  this.judgmentEngine = deps.judgmentEngine ?? new QuickModeJudgmentEngine();
42
+ this.fileExistencePort = deps.fileExistencePort;
35
43
  }
36
44
 
37
- async execute(
38
- input: ClassifyChangeCategoryUseCaseInput
39
- ): Promise<Readonly<ChangeCategoryClassificationContract>> {
45
+ /**
46
+ * targetChanges 引数自体が渡されない経路(CLI: check-change-category --paths)向けの
47
+ * changeKind 推定。hook 経路は beforeContent=null → CREATE と判定するため、同一パスで
48
+ * CLI=MODIFY(bugfix) / hook=CREATE(feature) に割れていた(WI-334)。
49
+ * ファイルが存在しない場合は CREATE、存在する場合は MODIFY と推定して hook 判定と一致させる。
50
+ * port 未注入・存在チェック失敗時は従来どおり MODIFY 既定(安全側)。
51
+ * なお hook は targetChanges を常に配列(空を含む)で渡すため推定対象外となり、
52
+ * Bash 抽出ターゲット等(配列にエントリが無いパス)の従来挙動は変えない。
53
+ */
54
+ private async inferChangeKind(filePath: string): Promise<ChangeKind> {
55
+ if (this.fileExistencePort === undefined) {
56
+ return "MODIFY";
57
+ }
58
+ try {
59
+ return (await this.fileExistencePort.exists(filePath)) ? "MODIFY" : "CREATE";
60
+ } catch {
61
+ return "MODIFY";
62
+ }
63
+ }
64
+
65
+ async execute(input: ClassifyChangeCategoryUseCaseInput): Promise<Readonly<ChangeCategoryClassificationContract>> {
40
66
  const config = await this.quickModeConfigPort.getConfig();
41
67
 
42
68
  if (input.paths.length === 0) {
@@ -47,21 +73,33 @@ export class ClassifyChangeCategoryUseCase {
47
73
  });
48
74
  }
49
75
 
76
+ const inferenceEnabled = input.targetChanges === undefined;
50
77
  const targetChanges = new Map((input.targetChanges ?? []).map((change) => [change.filePath, change]));
51
- const changedFiles = input.paths.map((p) => {
52
- const targetChange = targetChanges.get(p);
53
- const beforeContent = targetChange?.beforeContent ?? null;
54
- const afterContent = targetChange?.afterContent ?? null;
55
- return ChangedFile.create({
56
- filePath: p,
57
- // 変更前の内容が無く変更後の内容がある場合は新規作成 (CREATE) とみなす。
58
- // 以前は無条件で MODIFY 固定だったため、新規 domain/ ファイルが
59
- // NEW_DOMAIN 判定を回避して quick mode をすり抜けていた。
60
- changeKind: beforeContent === null && afterContent !== null ? 'CREATE' : 'MODIFY',
61
- beforeContent,
62
- afterContent,
63
- });
64
- });
78
+ const changedFiles = await Promise.all(
79
+ input.paths.map(async (p) => {
80
+ const targetChange = targetChanges.get(p);
81
+ if (targetChange === undefined && inferenceEnabled) {
82
+ // targetChanges 引数が渡されない経路(CLI)はファイル存在で CREATE/MODIFY を推定する(WI-334)
83
+ return ChangedFile.create({
84
+ filePath: p,
85
+ changeKind: await this.inferChangeKind(p),
86
+ beforeContent: null,
87
+ afterContent: null,
88
+ });
89
+ }
90
+ const beforeContent = targetChange?.beforeContent ?? null;
91
+ const afterContent = targetChange?.afterContent ?? null;
92
+ return ChangedFile.create({
93
+ filePath: p,
94
+ // 変更前の内容が無く変更後の内容がある場合は新規作成 (CREATE) とみなす。
95
+ // 以前は無条件で MODIFY 固定だったため、新規 domain/ ファイルが
96
+ // NEW_DOMAIN 判定を回避して quick mode をすり抜けていた。
97
+ changeKind: beforeContent === null && afterContent !== null ? "CREATE" : "MODIFY",
98
+ beforeContent,
99
+ afterContent,
100
+ });
101
+ }),
102
+ );
65
103
 
66
104
  const classification = this.judgmentEngine.classify(changedFiles, config);
67
105
  const eligibility = this.judgmentEngine.judge(changedFiles, config);
@@ -76,7 +114,7 @@ export class ClassifyChangeCategoryUseCase {
76
114
  categoryForFile = categoryKey;
77
115
  }
78
116
  });
79
- perFile.push({ path, category: categoryForFile ?? 'unknown' });
117
+ perFile.push({ path, category: categoryForFile ?? "unknown" });
80
118
  }
81
119
 
82
120
  const fullModeRequired = !eligibility.isEligible();
@@ -5,19 +5,20 @@
5
5
  * quick-mode ユニットの Composition Root
6
6
  */
7
7
 
8
- import { GitDiffChangedFilesAdapter } from './infrastructure/adapters/git-diff-changed-files-adapter.js';
9
- import { HarnessConfigQuickModeConfigAdapter } from './infrastructure/adapters/harness-config-quick-mode-config-adapter.js';
10
- import { ValidatorSystemValidatorIdRegistryAdapter } from './infrastructure/adapters/validator-system-validator-id-registry-adapter.js';
11
- import { ValidatorSystemQuickModeExecutionAdapter } from './infrastructure/adapters/validator-system-quick-mode-execution-adapter.js';
12
- import { QuickModeJudgmentEngine } from './domain/services/quick-mode-judgment-engine.js';
13
- import { ValidatorRelaxationService } from './domain/services/validator-relaxation-service.js';
14
- import { QuickModeDecisionContractMapper } from './application/mappers/quick-mode-decision-contract-mapper.js';
15
- import { JudgeQuickModeEligibilityUseCase } from './application/usecases/judge-quick-mode-eligibility-usecase.js';
16
- import { BuildRelaxationProfileUseCase } from './application/usecases/build-relaxation-profile-usecase.js';
17
- import { ExecuteQuickCiCheckUseCase } from './application/usecases/execute-quick-ci-check-usecase.js';
18
- import { ClassifyChangeCategoryUseCase } from './application/usecases/classify-change-category-usecase.js';
19
- import { CiCheckQuickModeHandler } from './presentation/handlers/ci-check-quick-mode-handler.js';
20
- import { CheckChangeCategoryHandler } from './presentation/handlers/check-change-category-handler.js';
8
+ import { QuickModeDecisionContractMapper } from "./application/mappers/quick-mode-decision-contract-mapper.js";
9
+ import { BuildRelaxationProfileUseCase } from "./application/usecases/build-relaxation-profile-usecase.js";
10
+ import { ClassifyChangeCategoryUseCase } from "./application/usecases/classify-change-category-usecase.js";
11
+ import { ExecuteQuickCiCheckUseCase } from "./application/usecases/execute-quick-ci-check-usecase.js";
12
+ import { JudgeQuickModeEligibilityUseCase } from "./application/usecases/judge-quick-mode-eligibility-usecase.js";
13
+ import { QuickModeJudgmentEngine } from "./domain/services/quick-mode-judgment-engine.js";
14
+ import { ValidatorRelaxationService } from "./domain/services/validator-relaxation-service.js";
15
+ import { FsFileExistenceAdapter } from "./infrastructure/adapters/fs-file-existence-adapter.js";
16
+ import { GitDiffChangedFilesAdapter } from "./infrastructure/adapters/git-diff-changed-files-adapter.js";
17
+ import { HarnessConfigQuickModeConfigAdapter } from "./infrastructure/adapters/harness-config-quick-mode-config-adapter.js";
18
+ import { ValidatorSystemQuickModeExecutionAdapter } from "./infrastructure/adapters/validator-system-quick-mode-execution-adapter.js";
19
+ import { ValidatorSystemValidatorIdRegistryAdapter } from "./infrastructure/adapters/validator-system-validator-id-registry-adapter.js";
20
+ import { CheckChangeCategoryHandler } from "./presentation/handlers/check-change-category-handler.js";
21
+ import { CiCheckQuickModeHandler } from "./presentation/handlers/ci-check-quick-mode-handler.js";
21
22
 
22
23
  export interface QuickModeCompositionRoot {
23
24
  handler: CiCheckQuickModeHandler;
@@ -28,12 +29,20 @@ export interface QuickModeCompositionRoot {
28
29
  classifyUseCase: ClassifyChangeCategoryUseCase;
29
30
  }
30
31
 
31
- export function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
32
+ export interface QuickModeCompositionRootOptions {
33
+ configPath?: string;
34
+ rootDir?: string;
35
+ }
36
+
37
+ export function createQuickModeCompositionRoot(
38
+ options: QuickModeCompositionRootOptions = {},
39
+ ): QuickModeCompositionRoot {
32
40
  // Infrastructure
33
41
  const gitDiffAdapter = new GitDiffChangedFilesAdapter();
34
- const harnessConfigAdapter = new HarnessConfigQuickModeConfigAdapter();
42
+ const harnessConfigAdapter = new HarnessConfigQuickModeConfigAdapter(options.configPath);
35
43
  const validatorIdRegistryAdapter = new ValidatorSystemValidatorIdRegistryAdapter();
36
44
  const validatorExecutionPort = new ValidatorSystemQuickModeExecutionAdapter();
45
+ const fileExistencePort = new FsFileExistenceAdapter(options.rootDir);
37
46
 
38
47
  // Domain Services
39
48
  const judgmentEngine = new QuickModeJudgmentEngine();
@@ -72,6 +81,7 @@ export function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
72
81
  const classifyUseCase = new ClassifyChangeCategoryUseCase({
73
82
  quickModeConfigPort,
74
83
  judgmentEngine,
84
+ fileExistencePort,
75
85
  });
76
86
 
77
87
  // Presentation
@@ -2,6 +2,8 @@
2
2
  * @layer domain
3
3
  * @unit quick-mode
4
4
  * @work-item-id WI-204
5
+ * @work-item-id WI-349
6
+ * @work-item-id WI-352
5
7
  *
6
8
  * ChangedFile[]をChangeClassificationに変換し、3拒否ルールを評価してQuickModeEligibilityを返すドメインサービス
7
9
  */
@@ -24,6 +26,37 @@ const RISK_PRIORITY: Record<string, number> = {
24
26
  docs: 0,
25
27
  };
26
28
 
29
+ // config: リポジトリ直下の bootstrap 設定ファイル(列挙型 allowlist)。
30
+ // これらは unit を持たず、CREATE だと feature に落ちて allowedCategories に
31
+ // 入れる手段がないため greenfield な初期セットアップが恒久的にブロックされていた。
32
+ // ワイルドカード(.github/** 等)には広げず、実在する bootstrap ファイル名のみを
33
+ // 列挙して fail-closed を維持する(WI-334 の .github/workflows、
34
+ // WI-261 の skills/**/*.md と同型の判断)。
35
+ // package.json は protected-file 経路で別途保護されるためここには含めない。
36
+ // @work-item-id WI-352
37
+ const ROOT_BOOTSTRAP_CONFIG_FILES: ReadonlySet<string> = new Set([
38
+ ".gitignore",
39
+ ".gitattributes",
40
+ ".editorconfig",
41
+ ".npmrc",
42
+ ".nvmrc",
43
+ "tsconfig.json",
44
+ ]);
45
+
46
+ const HUSKY_DIRECTORY_PREFIX = ".husky/";
47
+
48
+ function isRootBootstrapConfigFile(filePath: string): boolean {
49
+ if (ROOT_BOOTSTRAP_CONFIG_FILES.has(filePath)) {
50
+ return true;
51
+ }
52
+ // tsconfig.*.json(tsconfig.build.json / tsconfig.test.json 等)もルート直下のみ許可
53
+ if (/^tsconfig\.[^/]+\.json$/.test(filePath)) {
54
+ return true;
55
+ }
56
+ // .husky/ 配下の hook スクリプト(L0 の runtime 定義)
57
+ return filePath.startsWith(HUSKY_DIRECTORY_PREFIX) && filePath.slice(HUSKY_DIRECTORY_PREFIX.length).length > 0;
58
+ }
59
+
27
60
  function categorizeFile(file: ChangedFile): ChangeCategory {
28
61
  const { filePath, changeKind } = file;
29
62
 
@@ -37,6 +70,23 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
37
70
  return ChangeCategory.fromString("config");
38
71
  }
39
72
 
73
+ // config: .github/workflows/ 配下の CI workflow(.yml / .yaml)。
74
+ // CI workflow は unit を持たない構成ファイルであり、CREATE でも config に分類する。
75
+ // 内容レベルの防御は L3-006 injection scanner と integrity pin が担う
76
+ // (WI-261 の skills/**/*.md → docs 分類と同型の判断)。
77
+ // MODIFY がフォールバックで bugfix として quick を通過する現状との整合も取る。
78
+ // .github/ 全体には広げず workflows/ 配下の yml/yaml に限定し、
79
+ // それ以外の .github/ 配下は従来どおりフォールバック(fail-closed)を維持する。
80
+ // @work-item-id WI-334
81
+ if (filePath.startsWith(".github/workflows/") && (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))) {
82
+ return ChangeCategory.fromString("config");
83
+ }
84
+
85
+ // config: リポジトリ直下の bootstrap 設定ファイル(WI-352)
86
+ if (isRootBootstrapConfigFile(filePath)) {
87
+ return ChangeCategory.fromString("config");
88
+ }
89
+
40
90
  if (isCommentOnlyDiff(file)) {
41
91
  return ChangeCategory.fromString("docs");
42
92
  }
@@ -80,6 +130,17 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
80
130
  return ChangeCategory.fromString("bugfix");
81
131
  }
82
132
 
133
+ /**
134
+ * WI-349: 遮断理由に判定根拠(分類カテゴリと変更種別)を含める。
135
+ *
136
+ * 従来はパス列挙のみだったため「なぜそのパスが不許可なのか」が読み取れず、
137
+ * 利用者が「ワークツリーの無関係な変更のせいでブロックされている」と
138
+ * 誤認する原因になっていた(issue #41 症状②)。
139
+ */
140
+ function describeChangedFile(file: ChangedFile, category: string): string {
141
+ return `${file.filePath} (category=${category}, changeKind=${file.changeKind})`;
142
+ }
143
+
83
144
  export class QuickModeJudgmentEngine {
84
145
  classify(changedFiles: readonly ChangedFile[], _config?: QuickModeConfig): ChangeClassification {
85
146
  if (changedFiles.length === 0) {
@@ -115,9 +176,13 @@ export class QuickModeJudgmentEngine {
115
176
  // 1. MIXED_CHANGES評価: allowedCategories 外のカテゴリが含まれる場合
116
177
  if (config.isFullModeRequiredFor("mixedCategories")) {
117
178
  const notAllowedFiles: ChangedFile[] = [];
179
+ const notAllowedDescriptions: string[] = [];
118
180
  classification.categorizedFiles.forEach((files, categoryKey) => {
119
181
  if (!config.isAllowed(categoryKey)) {
120
182
  notAllowedFiles.push(...files);
183
+ for (const file of files) {
184
+ notAllowedDescriptions.push(describeChangedFile(file, categoryKey));
185
+ }
121
186
  }
122
187
  });
123
188
 
@@ -125,7 +190,7 @@ export class QuickModeJudgmentEngine {
125
190
  return QuickModeEligibility.rejected(
126
191
  "MIXED_CHANGES",
127
192
  notAllowedFiles,
128
- `allowedCategories外のファイルが含まれています: ${notAllowedFiles.map((f) => f.filePath).join(", ")}`,
193
+ `allowedCategories外のファイルが含まれています: ${notAllowedDescriptions.join(", ")}`,
129
194
  );
130
195
  }
131
196
  }
@@ -140,7 +205,9 @@ export class QuickModeJudgmentEngine {
140
205
  return QuickModeEligibility.rejected(
141
206
  "NEW_DOMAIN",
142
207
  newDomainFiles,
143
- `domain/配下に新規ファイルが追加されています: ${newDomainFiles.map((f) => f.filePath).join(", ")}`,
208
+ `domain/配下に新規ファイルが追加されています: ${newDomainFiles
209
+ .map((f) => describeChangedFile(f, categorizeFile(f).toString()))
210
+ .join(", ")}`,
144
211
  );
145
212
  }
146
213
  }
@@ -155,7 +222,9 @@ export class QuickModeJudgmentEngine {
155
222
  return QuickModeEligibility.rejected(
156
223
  "API_CONTRACT",
157
224
  apiContractFiles,
158
- `Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles.map((f) => f.filePath).join(", ")}`,
225
+ `Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles
226
+ .map((f) => describeChangedFile(f, categorizeFile(f).toString()))
227
+ .join(", ")}`,
159
228
  );
160
229
  }
161
230
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @layer infrastructure
3
+ * @unit quick-mode
4
+ * @work-item-id WI-334
5
+ *
6
+ * FileExistencePort の fs 実装。
7
+ * 相対パスは baseDir(既定: process.cwd())基準で解決する。
8
+ * stat が ENOENT / ENOTDIR 以外で失敗した場合(権限エラー等)は
9
+ * 「存在する」扱いにして呼び出し側の changeKind 推定を MODIFY 既定
10
+ * (従来どおりの安全側)に倒す。
11
+ */
12
+
13
+ import * as fs from "node:fs/promises";
14
+ import * as path from "node:path";
15
+ import type { FileExistencePort } from "../../application/ports/file-existence-port.js";
16
+
17
+ export class FsFileExistenceAdapter implements FileExistencePort {
18
+ private readonly baseDir: string;
19
+
20
+ constructor(baseDir?: string) {
21
+ this.baseDir = baseDir ?? process.cwd();
22
+ }
23
+
24
+ async exists(filePath: string): Promise<boolean> {
25
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(this.baseDir, filePath);
26
+ try {
27
+ await fs.stat(absolutePath);
28
+ return true;
29
+ } catch (err) {
30
+ const error = err as NodeJS.ErrnoException;
31
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") {
32
+ return false;
33
+ }
34
+ // stat 失敗(権限等)は存在扱い → changeKind は従来どおり MODIFY 既定(安全側)
35
+ return true;
36
+ }
37
+ }
38
+ }
@@ -2,26 +2,69 @@
2
2
  * @layer infrastructure
3
3
  * @unit skill-quality
4
4
  * @work-item-id WI-188
5
+ * @work-item-id WI-341
5
6
  */
6
- import { readFile } from 'node:fs/promises';
7
- import type { RequirementTestMatrixPort, RequirementTestMatrix } from '../../domain/ports/requirement-test-matrix-port.js';
8
- import { SkillQualityError } from '../../domain/errors/skill-quality-error.js';
7
+ import { readFile } from "node:fs/promises";
8
+ import { SkillQualityError } from "../../domain/errors/skill-quality-error.js";
9
+ import type {
10
+ RequirementTestMatrix,
11
+ RequirementTestMatrixPort,
12
+ } from "../../domain/ports/requirement-test-matrix-port.js";
13
+
14
+ interface GeneratedMatrixAcMapping {
15
+ readonly acId: string;
16
+ readonly testReferences: readonly unknown[];
17
+ }
18
+
19
+ interface GeneratedMatrixStory {
20
+ readonly storyId: string;
21
+ readonly storyMappings: readonly GeneratedMatrixAcMapping[];
22
+ }
9
23
 
10
24
  export class FileSystemRequirementTestMatrixAdapter implements RequirementTestMatrixPort {
11
- constructor(private readonly matrixFilePath: string = '.harness/requirement-test-matrix.json') {}
25
+ constructor(private readonly matrixFilePath: string = ".harness/requirement-test-matrix.json") {}
12
26
 
13
27
  async read(storyId: string): Promise<RequirementTestMatrix> {
14
28
  let raw: string;
15
29
  try {
16
- raw = await readFile(this.matrixFilePath, 'utf-8');
30
+ raw = await readFile(this.matrixFilePath, "utf-8");
17
31
  } catch {
18
- throw new SkillQualityError('MATRIX_FILE_NOT_FOUND', `requirement-test-matrix.json not found at ${this.matrixFilePath}`);
32
+ throw new SkillQualityError(
33
+ "MATRIX_FILE_NOT_FOUND",
34
+ `requirement-test-matrix.json not found at ${this.matrixFilePath}`,
35
+ );
19
36
  }
20
- const data = JSON.parse(raw) as Record<string, unknown>;
37
+ let data: Record<string, unknown>;
38
+ try {
39
+ data = JSON.parse(raw) as Record<string, unknown>;
40
+ } catch {
41
+ throw new SkillQualityError(
42
+ "MATRIX_FILE_NOT_FOUND",
43
+ `requirement-test-matrix.json could not be parsed at ${this.matrixFilePath}`,
44
+ );
45
+ }
46
+
47
+ if (Array.isArray(data.stories)) {
48
+ const story = (data.stories as GeneratedMatrixStory[]).find((candidate) => candidate.storyId === storyId);
49
+ if (story === undefined) {
50
+ throw new SkillQualityError("STORY_NOT_FOUND", `Story ${storyId} not found in ${this.matrixFilePath}`);
51
+ }
52
+
53
+ const uncoveredIds = story.storyMappings
54
+ .filter((mapping) => !Array.isArray(mapping.testReferences) || mapping.testReferences.length === 0)
55
+ .map((mapping) => mapping.acId);
56
+ return {
57
+ storyId,
58
+ total: story.storyMappings.length,
59
+ covered: story.storyMappings.length - uncoveredIds.length,
60
+ uncoveredIds,
61
+ };
62
+ }
63
+
21
64
  const rootEntry = data as { total?: unknown; covered?: unknown; uncoveredIds?: unknown };
22
65
  const explicitEntry = data[storyId];
23
66
  if (explicitEntry === undefined && rootEntry.total === undefined) {
24
- throw new SkillQualityError('STORY_NOT_FOUND', `Story ${storyId} not found in ${this.matrixFilePath}`);
67
+ throw new SkillQualityError("STORY_NOT_FOUND", `Story ${storyId} not found in ${this.matrixFilePath}`);
25
68
  }
26
69
  const entry = (explicitEntry ?? data) as { total: number; covered: number; uncoveredIds: string[] };
27
70
  return {
@@ -2,12 +2,14 @@
2
2
  * @layer presentation
3
3
  * @unit skill-quality
4
4
  * @work-item-id WI-188
5
+ * @work-item-id WI-341
5
6
  */
6
- import type { CheckCoverageUseCase } from '../../application/usecases/check-coverage-usecase.js';
7
+ import type { CheckCoverageUseCase } from "../../application/usecases/check-coverage-usecase.js";
8
+ import { SkillQualityError } from "../../domain/errors/skill-quality-error.js";
7
9
 
8
10
  export interface CheckCoverageArgs {
9
11
  storyId: string;
10
- format?: 'human' | 'json';
12
+ format?: "human" | "json";
11
13
  }
12
14
 
13
15
  export class CheckCoverageHandler {
@@ -16,9 +18,9 @@ export class CheckCoverageHandler {
16
18
  async handle(args: CheckCoverageArgs): Promise<{ exitCode: number; message: string }> {
17
19
  try {
18
20
  const output = await this.useCase.execute({ storyId: args.storyId });
19
- const format = args.format ?? 'human';
21
+ const format = args.format ?? "human";
20
22
 
21
- if (format === 'json') {
23
+ if (format === "json") {
22
24
  return { exitCode: output.meetsThreshold ? 0 : 1, message: JSON.stringify(output, null, 2) };
23
25
  }
24
26
 
@@ -26,7 +28,7 @@ export class CheckCoverageHandler {
26
28
  const codeRate = output.coverageReport.codeCoverage.lineCoverage.toFixed(1);
27
29
  const msg = `Requirement coverage: ${reqRate}% (threshold: ${output.requirementThreshold}%)\nCode coverage: ${codeRate}% (threshold: ${output.codeThreshold}%)`;
28
30
 
29
- if (output.skipped === true && output.skipReason === 'no-tests') {
31
+ if (output.skipped === true && output.skipReason === "no-tests") {
30
32
  return { exitCode: 0, message: `Coverage SKIPPED (no tests)\n${msg}` };
31
33
  }
32
34
 
@@ -35,7 +37,12 @@ export class CheckCoverageHandler {
35
37
  }
36
38
  return { exitCode: 1, message: `Coverage FAILED\n${msg}` };
37
39
  } catch (err) {
38
- return { exitCode: 2, message: `Error: ${err instanceof Error ? err.message : String(err)}` };
40
+ const message = err instanceof Error ? err.message : String(err);
41
+ if (args.format === "json") {
42
+ const code = err instanceof SkillQualityError ? err.code : "UNEXPECTED_ERROR";
43
+ return { exitCode: 2, message: JSON.stringify({ error: { code, message } }, null, 2) };
44
+ }
45
+ return { exitCode: 2, message: `Error: ${message}` };
39
46
  }
40
47
  }
41
48
  }
@@ -1,5 +1,6 @@
1
1
  // @unit traceability-model
2
2
  // @layer domain
3
+ // @work-item-id WI-337
3
4
  /**
4
5
  * WorkItemFrontmatter — 設計文書 frontmatter から抽出した WI メタデータ(H03-04 / ISSUE-026 Phase A-2)。
5
6
  *
@@ -8,7 +9,7 @@
8
9
  */
9
10
 
10
11
  export type WorkItemType = 'story' | 'issue' | 'fix' | 'refactor' | 'chore';
11
- export type WorkItemSeverity = 'trivial' | 'normal' | 'high';
12
+ export type WorkItemSeverity = 'trivial' | 'normal' | 'medium' | 'high' | 'critical' | 'major';
12
13
  export type WorkItemStatus =
13
14
  | 'drafted'
14
15
  | 'reflected'
@@ -47,7 +48,10 @@ export const WORK_ITEM_TYPES: ReadonlySet<WorkItemType> = new Set([
47
48
  export const WORK_ITEM_SEVERITIES: ReadonlySet<WorkItemSeverity> = new Set([
48
49
  'trivial',
49
50
  'normal',
51
+ 'medium',
50
52
  'high',
53
+ 'critical',
54
+ 'major',
51
55
  ]);
52
56
 
53
57
  export const WORK_ITEM_STATUSES: ReadonlySet<WorkItemStatus> = new Set([
@@ -1,6 +1,7 @@
1
1
  // @unit traceability-model
2
2
  // @layer infrastructure
3
3
  // @work-item-id WI-106
4
+ // @work-item-id WI-337
4
5
 
5
6
  import { readdir, readFile } from "node:fs/promises";
6
7
  import * as path from "node:path";
@@ -8,6 +9,7 @@ import type {
8
9
  WorkItemIdentityEntry,
9
10
  WorkItemIdentityPort,
10
11
  } from "../../domain/ports/work-item-identity-port.js";
12
+ import { WorkItemFrontmatterValidationError } from "../../domain/value-objects/work-item-frontmatter.js";
11
13
  import { parseWorkItemFrontmatter } from "../parsers/work-item-frontmatter-parser.js";
12
14
 
13
15
  const WI_DIR_PATTERN = /^WI-\d+$/;
@@ -67,7 +69,10 @@ export class FileSystemWorkItemIdentityGateway implements WorkItemIdentityPort {
67
69
  try {
68
70
  const content = await readFile(path.join(this.rootDir, descriptionPath), "utf8");
69
71
  return parseWorkItemFrontmatter(content)?.id ?? null;
70
- } catch {
72
+ } catch (error) {
73
+ if (error instanceof WorkItemFrontmatterValidationError) {
74
+ console.warn(`[phasegate] warning: ${descriptionPath} をスキップしました: ${error.message}`);
75
+ }
71
76
  return null;
72
77
  }
73
78
  }
@@ -1,11 +1,15 @@
1
1
  // @unit traceability-model
2
2
  // @layer infrastructure
3
3
  // @work-item-id WI-126 / WI-140
4
+ // @work-item-id WI-337
4
5
 
5
6
  import { readdir, readFile, writeFile } from "node:fs/promises";
6
7
  import * as path from "node:path";
7
8
  import type { WorkItemStatusPort } from "../../domain/ports/work-item-status-port.js";
8
- import type { WorkItemFrontmatter } from "../../domain/value-objects/work-item-frontmatter.js";
9
+ import {
10
+ type WorkItemFrontmatter,
11
+ WorkItemFrontmatterValidationError,
12
+ } from "../../domain/value-objects/work-item-frontmatter.js";
9
13
  import type {
10
14
  WorkItemStatusApplyResult,
11
15
  WorkItemStatusInput,
@@ -52,7 +56,16 @@ export class FileSystemWorkItemStatusGateway implements WorkItemStatusPort {
52
56
  const inputs: WorkItemStatusInput[] = [];
53
57
  for (const entry of entries) {
54
58
  const content = await readFile(path.join(this.rootDir, entry.descriptionPath), "utf8");
55
- const frontmatter = parseWorkItemFrontmatter(content);
59
+ let frontmatter: WorkItemFrontmatter | null;
60
+ try {
61
+ frontmatter = parseWorkItemFrontmatter(content);
62
+ } catch (error) {
63
+ if (error instanceof WorkItemFrontmatterValidationError) {
64
+ console.warn(`[phasegate] warning: ${entry.descriptionPath} をスキップしました: ${error.message}`);
65
+ continue;
66
+ }
67
+ throw error;
68
+ }
56
69
  if (frontmatter === null) continue;
57
70
 
58
71
  const aliases = this.aliasesFor(frontmatter);
@@ -4,9 +4,9 @@
4
4
  *
5
5
  * AggregateValidationResultsUseCase — H08-05: バリデータ結果統合集約
6
6
  */
7
- import type { ValidationResultContract } from '../dto/validation-result-contract.js';
8
- import type { AggregateResultsInput } from '../dto/aggregate-results-input.js';
9
- import type { AggregatedValidationReport } from '../dto/aggregated-validation-report.js';
7
+ import { isEffectivelyPassed } from "../../domain/services/effective-severity-policy.js";
8
+ import type { AggregateResultsInput } from "../dto/aggregate-results-input.js";
9
+ import type { AggregatedValidationReport } from "../dto/aggregated-validation-report.js";
10
10
 
11
11
  export class AggregateValidationResultsUseCase {
12
12
  execute(input: AggregateResultsInput): AggregatedValidationReport {
@@ -24,7 +24,7 @@ export class AggregateValidationResultsUseCase {
24
24
  }[] = [];
25
25
  let totalErrors = 0;
26
26
  let totalWarnings = 0;
27
- const errorsByLayer: Record<'L2' | 'L3' | 'L4', number> = { L2: 0, L3: 0, L4: 0 };
27
+ const errorsByLayer: Record<"L2" | "L3" | "L4", number> = { L2: 0, L3: 0, L4: 0 };
28
28
 
29
29
  for (const result of results) {
30
30
  if (result.skipped) {
@@ -32,12 +32,9 @@ 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');
37
- const hasWarnings = result.errors.some((e) => e.severity === 'warning');
38
- const isEmptyFail = !result.passed && result.errors.length === 0;
39
- const hasFail =
40
- !result.passed && (isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings));
35
+ // ADR-017 / WI-332: warning-only validator fail は failOnWarning=false で overall PASS、
36
+ // true で従来挙動 (FAIL)。実効判定は domain の共有実装 isEffectivelyPassed に一本化。
37
+ const hasFail = !isEffectivelyPassed(result, failOnWarning);
41
38
 
42
39
  if (hasFail) {
43
40
  failedValidators++;
@@ -54,7 +51,7 @@ export class AggregateValidationResultsUseCase {
54
51
  };
55
52
  allErrors.push(errorEntry);
56
53
 
57
- if (error.severity === 'warning') {
54
+ if (error.severity === "warning") {
58
55
  totalWarnings++;
59
56
  } else {
60
57
  totalErrors++;
@@ -62,9 +59,9 @@ export class AggregateValidationResultsUseCase {
62
59
 
63
60
  // レイヤー別集計
64
61
  const code = error.code;
65
- if (code.startsWith('L2-')) errorsByLayer.L2++;
66
- else if (code.startsWith('L3-')) errorsByLayer.L3++;
67
- else if (code.startsWith('L4-')) errorsByLayer.L4++;
62
+ if (code.startsWith("L2-")) errorsByLayer.L2++;
63
+ else if (code.startsWith("L3-")) errorsByLayer.L3++;
64
+ else if (code.startsWith("L4-")) errorsByLayer.L4++;
68
65
  }
69
66
  }
70
67