phasegate 0.283.0 → 0.335.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 (115) 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/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
  5. package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
  6. package/docs/guide/cli-reference.md +89 -7
  7. package/docs/guide/configuration.md +43 -2
  8. package/docs/guide/layer-model.md +2 -0
  9. package/docs/guide/quick-vs-full-mode.md +63 -3
  10. package/docs/guide/troubleshooting.md +37 -0
  11. package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
  12. package/package.json +2 -2
  13. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
  14. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -20
  15. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +215 -5
  16. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
  17. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
  18. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +18 -3
  19. package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
  20. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +57 -9
  21. package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
  22. package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
  23. package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
  24. package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
  25. package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
  26. package/scripts/harness/ci-governance/composition-root.ts +57 -5
  27. package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
  28. package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
  29. package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
  30. package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
  31. package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
  32. package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
  33. package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
  34. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
  35. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
  36. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
  37. package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
  38. package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
  39. package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
  40. package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
  41. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +3 -3
  42. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +3 -3
  43. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +3 -3
  44. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
  45. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
  46. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -3
  47. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
  48. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
  49. package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
  50. package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
  51. package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
  52. package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
  53. package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
  54. package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
  55. package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
  56. package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
  57. package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
  58. package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
  59. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
  60. package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
  61. package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
  62. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
  63. package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
  64. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
  65. package/scripts/harness/installation/application/usecases/run-install.ts +44 -11
  66. package/scripts/harness/installation/application/usecases/run-reconcile.ts +44 -11
  67. package/scripts/harness/installation/composition-root.ts +13 -3
  68. package/scripts/harness/installation/domain/check-id.ts +2 -0
  69. package/scripts/harness/installation/domain/config-status.ts +17 -0
  70. package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
  71. package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
  72. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
  73. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
  74. package/scripts/harness/integrations/pre-commit.ts +17 -3
  75. package/scripts/harness/main.ts +159 -21
  76. package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
  77. package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
  78. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +64 -33
  79. package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
  80. package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
  81. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
  82. package/scripts/harness/quick-mode/composition-root.ts +25 -15
  83. package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
  84. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +116 -18
  85. package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
  86. package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
  87. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
  88. package/scripts/harness/quick-mode/index.ts +7 -1
  89. package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
  90. package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
  91. package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
  92. package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
  93. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
  94. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
  95. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
  96. package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
  97. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +8 -0
  98. package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
  99. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
  100. package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
  101. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
  102. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
  103. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
  104. package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
  105. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
  106. package/skills/product-architect/SKILL.md +19 -0
  107. package/skills/quick-implementor/SKILL.md +19 -0
  108. package/skills/story-mapper/SKILL.md +11 -0
  109. package/skills/story-writer/SKILL.md +11 -0
  110. package/skills/unit-designer/SKILL.md +11 -0
  111. package/templates/product_overview.template.md +85 -0
  112. package/templates/product_overview_plan.template.md +55 -0
  113. package/templates/story_mapping_plan.template.md +61 -0
  114. package/templates/story_writer_plan.template.md +61 -0
  115. package/templates/unit_design_plan.template.md +63 -0
@@ -5,12 +5,14 @@
5
5
  * Quick Mode設定を表す値オブジェクト
6
6
  */
7
7
 
8
- export class QuickModeConfigError extends Error {
9
- constructor(message: string) {
10
- super(message);
11
- this.name = 'QuickModeConfigError';
12
- }
13
- }
8
+ import { QuickModeConfigError } from '../errors/quick-mode-config-error.js';
9
+ import {
10
+ CategoryOverrideRules,
11
+ type CategoryOverrideRulesInput,
12
+ } from './category-override-rules.js';
13
+ import { CHANGE_CATEGORY_VALUES, isChangeCategoryValue } from './change-category.js';
14
+
15
+ export { QuickModeConfigError };
14
16
 
15
17
  export type FullModeRequiredRuleId = 'mixedCategories' | 'newDomainFile' | 'apiContractChange';
16
18
 
@@ -32,17 +34,21 @@ export class QuickModeConfig {
32
34
  readonly maintainedLayers: readonly string[];
33
35
  readonly relaxedGates: readonly string[];
34
36
  readonly fullModeRequiredWhen: FullModeRequiredRules;
37
+ /** @work-item-id WI-372 プロジェクト固有パスのカテゴリ割当ルール(未設定時は空) */
38
+ readonly categoryOverrides: CategoryOverrideRules;
35
39
 
36
40
  private constructor(
37
41
  allowedCategories: readonly string[],
38
42
  maintainedLayers: readonly string[],
39
43
  relaxedGates: readonly string[],
40
- fullModeRequiredWhen: FullModeRequiredRules
44
+ fullModeRequiredWhen: FullModeRequiredRules,
45
+ categoryOverrides: CategoryOverrideRules
41
46
  ) {
42
47
  this.allowedCategories = allowedCategories;
43
48
  this.maintainedLayers = maintainedLayers;
44
49
  this.relaxedGates = relaxedGates;
45
50
  this.fullModeRequiredWhen = fullModeRequiredWhen;
51
+ this.categoryOverrides = categoryOverrides;
46
52
  Object.freeze(this);
47
53
  }
48
54
 
@@ -51,13 +57,26 @@ export class QuickModeConfig {
51
57
  maintainedLayers: string[];
52
58
  relaxedGates: string[];
53
59
  fullModeRequiredWhen?: Partial<FullModeRequiredRules>;
60
+ categoryOverrides?: CategoryOverrideRulesInput;
54
61
  }): QuickModeConfig {
55
- const { allowedCategories, maintainedLayers, relaxedGates, fullModeRequiredWhen } = raw;
62
+ const { allowedCategories, maintainedLayers, relaxedGates, fullModeRequiredWhen, categoryOverrides } = raw;
56
63
 
57
64
  if (allowedCategories.length === 0) {
58
65
  throw new QuickModeConfigError('allowedCategories must not be empty');
59
66
  }
60
67
 
68
+ // WI-373: allowedCategories は ChangeCategory 7 値の enum。
69
+ // 従来は非空チェックのみだったため "typoo" のような未知値が黙って通り、
70
+ // 「設定したのに効かない」状態になっていた。分類結果のキーは常に小文字なので
71
+ // 大文字小文字の正規化はせず厳密一致で拒否する(正規化は効かない設定の黙認になる)。
72
+ for (const category of allowedCategories) {
73
+ if (!isChangeCategoryValue(category)) {
74
+ throw new QuickModeConfigError(
75
+ `allowedCategories contains unknown category "${category}". Valid values: ${CHANGE_CATEGORY_VALUES.join(', ')}`
76
+ );
77
+ }
78
+ }
79
+
61
80
  const mergedRules: FullModeRequiredRules = Object.freeze({
62
81
  mixedCategories: fullModeRequiredWhen?.mixedCategories ?? DEFAULT_FULL_MODE_REQUIRED_WHEN.mixedCategories,
63
82
  newDomainFile: fullModeRequiredWhen?.newDomainFile ?? DEFAULT_FULL_MODE_REQUIRED_WHEN.newDomainFile,
@@ -68,7 +87,8 @@ export class QuickModeConfig {
68
87
  Object.freeze([...allowedCategories]),
69
88
  Object.freeze([...maintainedLayers]),
70
89
  Object.freeze([...relaxedGates]),
71
- mergedRules
90
+ mergedRules,
91
+ CategoryOverrideRules.create(categoryOverrides)
72
92
  );
73
93
  }
74
94
 
@@ -100,7 +120,8 @@ export class QuickModeConfig {
100
120
  JSON.stringify(this.relaxedGates) === JSON.stringify(other.relaxedGates) &&
101
121
  this.fullModeRequiredWhen.mixedCategories === other.fullModeRequiredWhen.mixedCategories &&
102
122
  this.fullModeRequiredWhen.newDomainFile === other.fullModeRequiredWhen.newDomainFile &&
103
- this.fullModeRequiredWhen.apiContractChange === other.fullModeRequiredWhen.apiContractChange
123
+ this.fullModeRequiredWhen.apiContractChange === other.fullModeRequiredWhen.apiContractChange &&
124
+ this.categoryOverrides.equals(other.categoryOverrides)
104
125
  );
105
126
  }
106
127
  }
@@ -1,7 +1,13 @@
1
1
  // @unit quick-mode
2
2
  // @layer application
3
3
 
4
- export { ChangeCategory, UnknownChangeCategoryError } from './domain/value-objects/change-category.js';
4
+ export {
5
+ CHANGE_CATEGORY_VALUES,
6
+ ChangeCategory,
7
+ isChangeCategoryValue,
8
+ UnknownChangeCategoryError,
9
+ } from './domain/value-objects/change-category.js';
10
+ export { CategoryOverrideRules } from './domain/value-objects/category-override-rules.js';
5
11
  export { ChangedFile } from './domain/value-objects/changed-file.js';
6
12
  export { ChangeClassification } from './domain/value-objects/change-classification.js';
7
13
  export { QuickModeConfig, QuickModeConfigError } from './domain/value-objects/quick-mode-config.js';
@@ -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,81 +2,132 @@
2
2
  * @layer infrastructure
3
3
  * @unit quick-mode
4
4
  * @work-item-id WI-140
5
+ * @work-item-id WI-372
6
+ * @work-item-id WI-377
5
7
  *
6
8
  * phasegate.config.json から QuickModeConfig を取得する Adapter
9
+ *
10
+ * ADR-040: 実効値は config-foundation の防御プリセット解決を経由して決定する。
11
+ * preset 定義(presets/*.json の quickMode)が実効値の所在であり、
12
+ * 下の DEFAULT_QUICK_MODE_CONFIG は preset 解決不能時の fail-open 用フォールバックである。
7
13
  */
8
14
 
9
- import * as fs from 'node:fs/promises';
10
- import * as path from 'node:path';
11
- import { QuickModeConfig } from '../../domain/value-objects/quick-mode-config.js';
15
+ import * as fs from "node:fs/promises";
16
+ import * as path from "node:path";
17
+ import type {
18
+ HarnessConfigResolvedDocument,
19
+ HarnessConfigSourceDocument,
20
+ PresetId,
21
+ } from "../../../config-foundation/domain/harness-config.js";
22
+ import { PresetResolutionService } from "../../../config-foundation/domain/services/preset-resolution-service.js";
23
+ import { PresetDefinitionStore } from "../../../config-foundation/infrastructure/preset-definition-store.js";
24
+ import { QuickModeConfig } from "../../domain/value-objects/quick-mode-config.js";
12
25
 
13
26
  export class HarnessConfigNotFoundError extends Error {
14
27
  constructor(filePath: string) {
15
28
  super(`phasegate.config.json not found: ${filePath}`);
16
- this.name = 'HarnessConfigNotFoundError';
29
+ this.name = "HarnessConfigNotFoundError";
17
30
  }
18
31
  }
19
32
 
20
33
  export class HarnessConfigParseError extends Error {
21
34
  constructor(message: string) {
22
35
  super(`Failed to parse phasegate.config.json: ${message}`);
23
- this.name = 'HarnessConfigParseError';
36
+ this.name = "HarnessConfigParseError";
24
37
  }
25
38
  }
26
39
 
40
+ /**
41
+ * preset 解決が使えない config(未知 preset / project 欠落 / 他セクション不正)でも
42
+ * Quick Mode 判定を止めないためのフォールバック。ADR-038 §3-1 の fail-open 原則に従い、
43
+ * preset 解決の導入によって新たな遮断経路を作らない。
44
+ */
27
45
  const DEFAULT_QUICK_MODE_CONFIG = {
28
- allowedCategories: ['bugfix', 'docs', 'test', 'config'],
29
- maintainedLayers: ['L1', 'L2-002', 'L2-003', 'L2-014', 'L3-001'],
30
- relaxedGates: ['L2-001', 'L3-002', 'L3-003', 'L3-004', 'L4'],
46
+ allowedCategories: ["bugfix", "docs", "test", "config"],
47
+ maintainedLayers: ["L1", "L2-002", "L2-003", "L2-014", "L3-001"],
48
+ relaxedGates: ["L2-001", "L3-002", "L3-003", "L3-004", "L4"],
31
49
  };
32
50
 
51
+ const PRESET_IDS: readonly PresetId[] = ["minimal", "standard", "strict"];
52
+
53
+ type RawQuickMode = HarnessConfigResolvedDocument["quickMode"];
54
+
55
+ function isPresetId(value: unknown): value is PresetId {
56
+ return typeof value === "string" && PRESET_IDS.includes(value as PresetId);
57
+ }
58
+
33
59
  export class HarnessConfigQuickModeConfigAdapter {
34
60
  private readonly configPath: string;
61
+ private readonly presetDefinitionStore: PresetDefinitionStore;
62
+ private readonly presetResolutionService: PresetResolutionService;
35
63
 
36
64
  constructor(configPath?: string) {
37
- this.configPath = configPath ?? path.resolve(process.cwd(), 'phasegate.config.json');
65
+ this.configPath = configPath ?? path.resolve(process.cwd(), "phasegate.config.json");
66
+ this.presetDefinitionStore = new PresetDefinitionStore();
67
+ this.presetResolutionService = new PresetResolutionService();
38
68
  }
39
69
 
40
70
  async getQuickModeConfig(): Promise<QuickModeConfig> {
71
+ const parsed = await this.readSourceDocument();
72
+ const quickMode = this.resolveQuickMode(parsed);
73
+
74
+ return QuickModeConfig.create({
75
+ allowedCategories: quickMode?.allowedCategories ?? DEFAULT_QUICK_MODE_CONFIG.allowedCategories,
76
+ maintainedLayers: quickMode?.maintainedLayers ?? DEFAULT_QUICK_MODE_CONFIG.maintainedLayers,
77
+ relaxedGates: quickMode?.relaxedGates ?? DEFAULT_QUICK_MODE_CONFIG.relaxedGates,
78
+ fullModeRequiredWhen: quickMode?.fullModeRequiredWhen,
79
+ // WI-372 × WI-377 合流点: categoryOverrides は preset 解決経路と raw 縮退経路の
80
+ // 双方が返す `quickMode` から一様に読む。preset 定義は当該キーを宣言しないため
81
+ // deepMerge は source の宣言をそのまま引き継ぎ(base 側 undefined → override を clone)、
82
+ // preset 解決が例外で縮退した場合も raw の宣言がそのまま渡る。未設定時は空ルール
83
+ // = 現行分類のまま。enum 違反は QuickModeConfig.create が QuickModeConfigError を
84
+ // 投げ、この try/catch の外側なので fail-closed 経路へ表面化する。
85
+ categoryOverrides: quickMode?.categoryOverrides,
86
+ });
87
+ }
88
+
89
+ private async readSourceDocument(): Promise<Record<string, unknown>> {
41
90
  let content: string;
42
91
 
43
92
  try {
44
- content = await fs.readFile(this.configPath, 'utf8') as string;
93
+ content = (await fs.readFile(this.configPath, "utf8")) as string;
45
94
  } catch (err) {
46
95
  const error = err as NodeJS.ErrnoException;
47
- if (error.code === 'ENOENT') {
96
+ if (error.code === "ENOENT") {
48
97
  throw new HarnessConfigNotFoundError(this.configPath);
49
98
  }
50
99
  throw err;
51
100
  }
52
101
 
53
- let parsed: Record<string, unknown>;
54
102
  try {
55
- parsed = JSON.parse(content) as Record<string, unknown>;
103
+ return JSON.parse(content) as Record<string, unknown>;
56
104
  } catch (err) {
57
105
  throw new HarnessConfigParseError(err instanceof Error ? err.message : String(err));
58
106
  }
107
+ }
108
+
109
+ /**
110
+ * 防御プリセット解決を経由した quickMode を返す。
111
+ * preset が特定できない、または解決が失敗する config では raw の quickMode に縮退し、
112
+ * 未宣言キーは呼び出し側で DEFAULT_QUICK_MODE_CONFIG に補完される。
113
+ */
114
+ private resolveQuickMode(parsed: Record<string, unknown>): Partial<RawQuickMode> | undefined {
115
+ const rawQuickMode = parsed["quickMode"] as Partial<RawQuickMode> | undefined;
116
+ const preset = (parsed["project"] as { preset?: unknown } | undefined)?.preset;
59
117
 
60
- const quickMode = parsed['quickMode'] as {
61
- allowedCategories?: string[];
62
- maintainedLayers?: string[];
63
- relaxedGates?: string[];
64
- fullModeRequiredWhen?: {
65
- mixedCategories?: boolean;
66
- newDomainFile?: boolean;
67
- apiContractChange?: boolean;
68
- };
69
- } | undefined;
70
-
71
- if (!quickMode) {
72
- return QuickModeConfig.create(DEFAULT_QUICK_MODE_CONFIG);
118
+ if (!isPresetId(preset)) {
119
+ return rawQuickMode;
73
120
  }
74
121
 
75
- return QuickModeConfig.create({
76
- allowedCategories: quickMode.allowedCategories ?? DEFAULT_QUICK_MODE_CONFIG.allowedCategories,
77
- maintainedLayers: quickMode.maintainedLayers ?? DEFAULT_QUICK_MODE_CONFIG.maintainedLayers,
78
- relaxedGates: quickMode.relaxedGates ?? DEFAULT_QUICK_MODE_CONFIG.relaxedGates,
79
- fullModeRequiredWhen: quickMode.fullModeRequiredWhen,
80
- });
122
+ try {
123
+ const presetDefinitions = this.presetDefinitionStore.load();
124
+ const resolved = this.presetResolutionService.resolve(
125
+ parsed as unknown as HarnessConfigSourceDocument,
126
+ presetDefinitions[preset],
127
+ );
128
+ return resolved.quickMode;
129
+ } catch {
130
+ return rawQuickMode;
131
+ }
81
132
  }
82
133
  }
@@ -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
 
@@ -171,6 +171,9 @@ export class RunL3ValidatorsUseCase {
171
171
  severity: "error",
172
172
  message: `カバレッジ不足: 現在値 ${coverageData.overallCoverage}%、不足 ${threshold - coverageData.overallCoverage}%`,
173
173
  suggestion: `テストカバレッジを ${threshold}% 以上に引き上げてください`,
174
+ // WI-335: 閾値未達の解消はテスト追加(AI/人間の判断)が必要。
175
+ // L3-003 の registry 既定は mechanical だが、この finding は機械適用不能なので明示的に上書きする。
176
+ remediationType: "ai-assisted",
174
177
  },
175
178
  ],
176
179
  0,
@@ -190,8 +193,13 @@ export class RunL3ValidatorsUseCase {
190
193
  code: "L3-003",
191
194
  severity: "error",
192
195
  message: `coverageThreshold=${threshold}% が設定されていますがカバレッジレポートが見つかりません(テストをカバレッジ付きで実行してください)`,
196
+ // WI-335: 選択肢 (b)「layers.L3.coverageThreshold を 0 に設定する」は config 編集のみで
197
+ // 完結する機械適用可能な opt-out(WI-317 / github#37)なので mechanical と宣言する。
198
+ // この文言(layers.L3.coverageThreshold を 0)を変える場合は remediation-round-trip
199
+ // テスト(機械適用器が同じ文言を解析する)が fail する。
193
200
  suggestion:
194
201
  '次のいずれかで解消してください: (a) テストをカバレッジ付きで実行してレポートを生成する(例: vitest --coverage)、(b) カバレッジゲートを opt-out するなら config の layers.L3.coverageThreshold を 0 に設定する、(c) 非 JS/TS プロジェクトなら project.languages を宣言する(例: ["python"]。L3-003 自体が unsupported-language SKIP になる)',
202
+ remediationType: "mechanical",
195
203
  },
196
204
  ],
197
205
  0,
@@ -0,0 +1,39 @@
1
+ // @unit validator-system
2
+ // @layer domain
3
+ // @work-item-id WI-332
4
+
5
+ /**
6
+ * effective-severity-policy — 実効 severity 判定の単一ソース (ADR-017 / ADR-021)。
7
+ *
8
+ * validator 1件が「実質 pass」かを severity-aware に判定するルール。
9
+ * WI-332 以前は同じ判定式が 3 箇所(validate 集約 usecase / harness-api CiCheckResult /
10
+ * pre-commit の手動集約)に複製・乖離しており、#38(complete-check だけ warning-only で
11
+ * exit 1)型の回帰を招いた。以後、実効判定は必ずこの関数を経由すること。
12
+ *
13
+ * 判定ルール(ADR-017 Decision の集計セマンティクスそのもの):
14
+ * - skipped: 実質 pass
15
+ * - passed=true: 実質 pass
16
+ * - passed=false かつ error severity(!= warning)を含む: fail
17
+ * - passed=false かつ errors=[](severity 判定不能): 安全側に倒して fail
18
+ * - passed=false かつ warning のみ: failOnWarning=false(既定)で実質 pass、true で fail
19
+ */
20
+
21
+ /**
22
+ * 判定に必要な最小構造。validator-system の ValidationResultContract と
23
+ * harness-api の ValidatorCheckItem の双方が構造的に満たす。
24
+ */
25
+ export interface EffectiveSeverityCheckItem {
26
+ readonly passed: boolean;
27
+ readonly skipped?: boolean;
28
+ readonly errors?: readonly { readonly severity: string }[];
29
+ }
30
+
31
+ export function isEffectivelyPassed(item: EffectiveSeverityCheckItem, failOnWarning = false): boolean {
32
+ if (item.skipped || item.passed) return true;
33
+ const errors = item.errors ?? [];
34
+ const hasNonWarningError = errors.some((e) => e.severity !== "warning");
35
+ const hasWarnings = errors.some((e) => e.severity === "warning");
36
+ const isEmptyFail = errors.length === 0;
37
+ const hasFail = isEmptyFail || hasNonWarningError || (failOnWarning && hasWarnings);
38
+ return !hasFail;
39
+ }