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
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @layer application
3
+ * @unit quick-mode
4
+ * @work-item-id WI-334
5
+ *
6
+ * ファイルの存在確認を行う Application 層 Port。
7
+ * ClassifyChangeCategoryUseCase が targetChanges を受け取らない経路
8
+ * (CLI: check-change-category --paths)で changeKind を CREATE/MODIFY と
9
+ * 推定するために使用する。hook 経路(beforeContent=null → CREATE)との
10
+ * 判定一致を保証する目的で導入された(WI-334)。
11
+ */
12
+
13
+ export interface FileExistencePort {
14
+ exists(filePath: string): Promise<boolean>;
15
+ }
@@ -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
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit quick-mode
4
+ * @work-item-id WI-372
5
+ *
6
+ * QuickMode 設定の不変条件違反を表すエラー。
7
+ *
8
+ * `quick-mode-config.ts` と `category-override-rules.ts` の双方から参照されるため、
9
+ * 循環 import を避ける目的で独立ファイルに切り出している。
10
+ * 後方互換のため `quick-mode-config.ts` からも re-export される。
11
+ */
12
+
13
+ export class QuickModeConfigError extends Error {
14
+ constructor(message: string) {
15
+ super(message);
16
+ this.name = 'QuickModeConfigError';
17
+ Object.setPrototypeOf(this, new.target.prototype);
18
+ }
19
+ }
@@ -2,10 +2,14 @@
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
7
+ * @work-item-id WI-372
5
8
  *
6
9
  * ChangedFile[]をChangeClassificationに変換し、3拒否ルールを評価してQuickModeEligibilityを返すドメインサービス
7
10
  */
8
11
 
12
+ import { CategoryOverrideRules } from "../value-objects/category-override-rules.js";
9
13
  import { ChangeCategory } from "../value-objects/change-category.js";
10
14
  import { ChangeClassification } from "../value-objects/change-classification.js";
11
15
  import type { ChangedFile } from "../value-objects/changed-file.js";
@@ -13,18 +17,38 @@ import type { QuickModeConfig } from "../value-objects/quick-mode-config.js";
13
17
  import { QuickModeEligibility } from "../value-objects/quick-mode-eligibility.js";
14
18
  import { isCommentOnlyDiff } from "./comment-only-diff-detector.js";
15
19
 
16
- // リスク順優先度(api > domain > feature > bugfix > test > config > docs)
17
- const RISK_PRIORITY: Record<string, number> = {
18
- api: 6,
19
- domain: 5,
20
- feature: 4,
21
- bugfix: 3,
22
- test: 2,
23
- config: 1,
24
- docs: 0,
25
- };
26
-
27
- function categorizeFile(file: ChangedFile): ChangeCategory {
20
+ // config: リポジトリ直下の bootstrap 設定ファイル(列挙型 allowlist)。
21
+ // これらは unit を持たず、CREATE だと feature に落ちて allowedCategories に
22
+ // 入れる手段がないため greenfield な初期セットアップが恒久的にブロックされていた。
23
+ // ワイルドカード(.github/** 等)には広げず、実在する bootstrap ファイル名のみを
24
+ // 列挙して fail-closed を維持する(WI-334 の .github/workflows、
25
+ // WI-261 の skills/**/*.md と同型の判断)。
26
+ // package.json は protected-file 経路で別途保護されるためここには含めない。
27
+ // @work-item-id WI-352
28
+ const ROOT_BOOTSTRAP_CONFIG_FILES: ReadonlySet<string> = new Set([
29
+ ".gitignore",
30
+ ".gitattributes",
31
+ ".editorconfig",
32
+ ".npmrc",
33
+ ".nvmrc",
34
+ "tsconfig.json",
35
+ ]);
36
+
37
+ const HUSKY_DIRECTORY_PREFIX = ".husky/";
38
+
39
+ function isRootBootstrapConfigFile(filePath: string): boolean {
40
+ if (ROOT_BOOTSTRAP_CONFIG_FILES.has(filePath)) {
41
+ return true;
42
+ }
43
+ // tsconfig.*.json(tsconfig.build.json / tsconfig.test.json 等)もルート直下のみ許可
44
+ if (/^tsconfig\.[^/]+\.json$/.test(filePath)) {
45
+ return true;
46
+ }
47
+ // .husky/ 配下の hook スクリプト(L0 の runtime 定義)
48
+ return filePath.startsWith(HUSKY_DIRECTORY_PREFIX) && filePath.slice(HUSKY_DIRECTORY_PREFIX.length).length > 0;
49
+ }
50
+
51
+ function categorizeFileByBuiltInRules(file: ChangedFile): ChangeCategory {
28
52
  const { filePath, changeKind } = file;
29
53
 
30
54
  // Config files must stay config even when Edit payload snippets look like
@@ -37,6 +61,23 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
37
61
  return ChangeCategory.fromString("config");
38
62
  }
39
63
 
64
+ // config: .github/workflows/ 配下の CI workflow(.yml / .yaml)。
65
+ // CI workflow は unit を持たない構成ファイルであり、CREATE でも config に分類する。
66
+ // 内容レベルの防御は L3-006 injection scanner と integrity pin が担う
67
+ // (WI-261 の skills/**/*.md → docs 分類と同型の判断)。
68
+ // MODIFY がフォールバックで bugfix として quick を通過する現状との整合も取る。
69
+ // .github/ 全体には広げず workflows/ 配下の yml/yaml に限定し、
70
+ // それ以外の .github/ 配下は従来どおりフォールバック(fail-closed)を維持する。
71
+ // @work-item-id WI-334
72
+ if (filePath.startsWith(".github/workflows/") && (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))) {
73
+ return ChangeCategory.fromString("config");
74
+ }
75
+
76
+ // config: リポジトリ直下の bootstrap 設定ファイル(WI-352)
77
+ if (isRootBootstrapConfigFile(filePath)) {
78
+ return ChangeCategory.fromString("config");
79
+ }
80
+
40
81
  if (isCommentOnlyDiff(file)) {
41
82
  return ChangeCategory.fromString("docs");
42
83
  }
@@ -80,18 +121,67 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
80
121
  return ChangeCategory.fromString("bugfix");
81
122
  }
82
123
 
124
+ // 組み込み判定が構造的に高リスク(domain / api)のカテゴリは override で降格できない。
125
+ // @work-item-id WI-372
126
+ const NON_DOWNGRADABLE_BUILT_IN_CATEGORIES: ReadonlySet<string> = new Set(["domain", "api"]);
127
+
128
+ /**
129
+ * WI-372: `quickMode.categoryOverrides` を反映したカテゴリ判定。
130
+ *
131
+ * override は「このパスはこの種類の変更である」という利用者の明示宣言なので、
132
+ * 組み込みルールより **先** に評価する(DD-1)。組み込みの後段に置くと
133
+ * `notes/x.config.json` のような偶発的な組み込みマッチに常に負け、
134
+ * 「設定したのに効かない」不可解な挙動になるため。
135
+ *
136
+ * ただし組み込み判定が `domain` / `api` のファイルは override で降格できない(DD-2)。
137
+ * `judge()` の NEW_DOMAIN は CREATE のみ、API_CONTRACT は port/adapter のみを見るため、
138
+ * ガードが無いと domain ファイルの MODIFY が `docs` として素通りしてしまう。
139
+ * 降格は禁じるが昇格(domain → api 等)は許す。
140
+ *
141
+ * override 未設定時は組み込み分類と完全に一致する(後方互換)。
142
+ */
143
+ function categorizeFile(file: ChangedFile, overrides: CategoryOverrideRules): ChangeCategory {
144
+ const builtIn = categorizeFileByBuiltInRules(file);
145
+ if (overrides.isEmpty()) {
146
+ return builtIn;
147
+ }
148
+
149
+ const override = overrides.resolve(file.filePath);
150
+ if (override === null) {
151
+ return builtIn;
152
+ }
153
+
154
+ if (NON_DOWNGRADABLE_BUILT_IN_CATEGORIES.has(builtIn.toString())) {
155
+ return override.riskPriority() > builtIn.riskPriority() ? override : builtIn;
156
+ }
157
+
158
+ return override;
159
+ }
160
+
161
+ /**
162
+ * WI-349: 遮断理由に判定根拠(分類カテゴリと変更種別)を含める。
163
+ *
164
+ * 従来はパス列挙のみだったため「なぜそのパスが不許可なのか」が読み取れず、
165
+ * 利用者が「ワークツリーの無関係な変更のせいでブロックされている」と
166
+ * 誤認する原因になっていた(issue #41 症状②)。
167
+ */
168
+ function describeChangedFile(file: ChangedFile, category: string): string {
169
+ return `${file.filePath} (category=${category}, changeKind=${file.changeKind})`;
170
+ }
171
+
83
172
  export class QuickModeJudgmentEngine {
84
- classify(changedFiles: readonly ChangedFile[], _config?: QuickModeConfig): ChangeClassification {
173
+ classify(changedFiles: readonly ChangedFile[], config?: QuickModeConfig): ChangeClassification {
85
174
  if (changedFiles.length === 0) {
86
175
  return new ChangeClassification(null, new Map(), 0);
87
176
  }
88
177
 
178
+ const overrides = config?.categoryOverrides ?? CategoryOverrideRules.empty();
89
179
  const categorizedMap = new Map<string, ChangedFile[]>();
90
180
  let dominantCategory: ChangeCategory | null = null;
91
181
  let dominantPriority = -1;
92
182
 
93
183
  for (const file of changedFiles) {
94
- const category = categorizeFile(file);
184
+ const category = categorizeFile(file, overrides);
95
185
  const key = category.toString();
96
186
 
97
187
  if (!categorizedMap.has(key)) {
@@ -99,7 +189,7 @@ export class QuickModeJudgmentEngine {
99
189
  }
100
190
  categorizedMap.get(key)!.push(file);
101
191
 
102
- const priority = RISK_PRIORITY[key] ?? 0;
192
+ const priority = category.riskPriority();
103
193
  if (priority > dominantPriority) {
104
194
  dominantPriority = priority;
105
195
  dominantCategory = category;
@@ -115,9 +205,13 @@ export class QuickModeJudgmentEngine {
115
205
  // 1. MIXED_CHANGES評価: allowedCategories 外のカテゴリが含まれる場合
116
206
  if (config.isFullModeRequiredFor("mixedCategories")) {
117
207
  const notAllowedFiles: ChangedFile[] = [];
208
+ const notAllowedDescriptions: string[] = [];
118
209
  classification.categorizedFiles.forEach((files, categoryKey) => {
119
210
  if (!config.isAllowed(categoryKey)) {
120
211
  notAllowedFiles.push(...files);
212
+ for (const file of files) {
213
+ notAllowedDescriptions.push(describeChangedFile(file, categoryKey));
214
+ }
121
215
  }
122
216
  });
123
217
 
@@ -125,7 +219,7 @@ export class QuickModeJudgmentEngine {
125
219
  return QuickModeEligibility.rejected(
126
220
  "MIXED_CHANGES",
127
221
  notAllowedFiles,
128
- `allowedCategories外のファイルが含まれています: ${notAllowedFiles.map((f) => f.filePath).join(", ")}`,
222
+ `allowedCategories外のファイルが含まれています: ${notAllowedDescriptions.join(", ")}`,
129
223
  );
130
224
  }
131
225
  }
@@ -140,7 +234,9 @@ export class QuickModeJudgmentEngine {
140
234
  return QuickModeEligibility.rejected(
141
235
  "NEW_DOMAIN",
142
236
  newDomainFiles,
143
- `domain/配下に新規ファイルが追加されています: ${newDomainFiles.map((f) => f.filePath).join(", ")}`,
237
+ `domain/配下に新規ファイルが追加されています: ${newDomainFiles
238
+ .map((f) => describeChangedFile(f, categorizeFile(f, config.categoryOverrides).toString()))
239
+ .join(", ")}`,
144
240
  );
145
241
  }
146
242
  }
@@ -155,7 +251,9 @@ export class QuickModeJudgmentEngine {
155
251
  return QuickModeEligibility.rejected(
156
252
  "API_CONTRACT",
157
253
  apiContractFiles,
158
- `Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles.map((f) => f.filePath).join(", ")}`,
254
+ `Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles
255
+ .map((f) => describeChangedFile(f, categorizeFile(f, config.categoryOverrides).toString()))
256
+ .join(", ")}`,
159
257
  );
160
258
  }
161
259
  }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @layer domain
3
+ * @unit quick-mode
4
+ * @work-item-id WI-372
5
+ *
6
+ * `quickMode.categoryOverrides` を表す値オブジェクト。
7
+ * ChangeCategory 7 値をキー、glob パターン列を値とする写像で、
8
+ * プロジェクト固有パス(`results/**` 等)を任意カテゴリへ割り当てる。
9
+ */
10
+
11
+ import { QuickModeConfigError } from '../errors/quick-mode-config-error.js';
12
+ import {
13
+ CHANGE_CATEGORY_VALUES,
14
+ ChangeCategory,
15
+ type ChangeCategoryValue,
16
+ isChangeCategoryValue,
17
+ } from './change-category.js';
18
+
19
+ /**
20
+ * glob → 正規表現の変換。
21
+ *
22
+ * domain 層は repo 全体で外部 npm パッケージを import していないため
23
+ * (picomatch は infrastructure adapter 専用)、
24
+ * `agent-integration/domain/value-objects/protected-file-list.ts` と同型の
25
+ * 純正規表現実装を採る(logical_design.md LD-10)。
26
+ *
27
+ * サポートする記法:
28
+ * - `**` … `/` を含む任意の文字列
29
+ * - `*` … `/` を含まない任意の文字列
30
+ * - `?` … `/` 以外の 1 文字
31
+ * - その他はリテラル(正規表現メタ文字はエスケープ)
32
+ */
33
+ function globToRegExp(pattern: string): RegExp {
34
+ const regexSource = pattern
35
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
36
+ .replace(/\*\*/g, '__DOUBLE_STAR__')
37
+ .replace(/\*/g, '[^/]*')
38
+ .replace(/__DOUBLE_STAR__/g, '.*')
39
+ .replace(/\?/g, '[^/]');
40
+ return new RegExp(`^${regexSource}$`);
41
+ }
42
+
43
+ export type CategoryOverrideRulesInput = Readonly<Record<string, readonly string[]>>;
44
+
45
+ export class CategoryOverrideRules {
46
+ private readonly patterns: ReadonlyMap<ChangeCategoryValue, readonly string[]>;
47
+ private readonly matchers: ReadonlyMap<ChangeCategoryValue, readonly RegExp[]>;
48
+
49
+ private constructor(
50
+ patterns: ReadonlyMap<ChangeCategoryValue, readonly string[]>,
51
+ matchers: ReadonlyMap<ChangeCategoryValue, readonly RegExp[]>,
52
+ ) {
53
+ this.patterns = patterns;
54
+ this.matchers = matchers;
55
+ Object.freeze(this);
56
+ }
57
+
58
+ static empty(): CategoryOverrideRules {
59
+ return new CategoryOverrideRules(new Map(), new Map());
60
+ }
61
+
62
+ static create(raw: CategoryOverrideRulesInput | undefined | null): CategoryOverrideRules {
63
+ if (raw === undefined || raw === null) {
64
+ return CategoryOverrideRules.empty();
65
+ }
66
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
67
+ throw new QuickModeConfigError('categoryOverrides must be an object keyed by change category');
68
+ }
69
+
70
+ const patterns = new Map<ChangeCategoryValue, readonly string[]>();
71
+ const matchers = new Map<ChangeCategoryValue, readonly RegExp[]>();
72
+
73
+ for (const [key, value] of Object.entries(raw)) {
74
+ if (!isChangeCategoryValue(key)) {
75
+ throw new QuickModeConfigError(
76
+ `categoryOverrides contains unknown category "${key}". Valid values: ${CHANGE_CATEGORY_VALUES.join(', ')}`,
77
+ );
78
+ }
79
+ if (!Array.isArray(value)) {
80
+ throw new QuickModeConfigError(`categoryOverrides.${key} must be an array of glob patterns`);
81
+ }
82
+ for (const pattern of value) {
83
+ if (typeof pattern !== 'string' || pattern.length === 0) {
84
+ throw new QuickModeConfigError(
85
+ `categoryOverrides.${key} must not contain an empty or non-string glob pattern`,
86
+ );
87
+ }
88
+ }
89
+ if (value.length === 0) {
90
+ continue;
91
+ }
92
+ patterns.set(key, Object.freeze([...value]));
93
+ matchers.set(key, Object.freeze(value.map(globToRegExp)));
94
+ }
95
+
96
+ return new CategoryOverrideRules(patterns, matchers);
97
+ }
98
+
99
+ isEmpty(): boolean {
100
+ return this.matchers.size === 0;
101
+ }
102
+
103
+ /**
104
+ * パスに対応する override カテゴリを返す。
105
+ * 一致が無ければ null(組み込み分類へ委譲)。
106
+ * 複数カテゴリに一致した場合はリスク優先度が最も高いカテゴリを返すため、
107
+ * JSON のキー列挙順に依存しない決定的な結果になる(domain_model.md DD-4)。
108
+ */
109
+ resolve(filePath: string): ChangeCategory | null {
110
+ if (filePath === '' || this.matchers.size === 0) {
111
+ return null;
112
+ }
113
+
114
+ let resolved: ChangeCategory | null = null;
115
+ let resolvedPriority = -1;
116
+
117
+ for (const [category, regexps] of this.matchers) {
118
+ if (!regexps.some((regexp) => regexp.test(filePath))) {
119
+ continue;
120
+ }
121
+ const candidate = ChangeCategory.fromString(category);
122
+ const priority = candidate.riskPriority();
123
+ if (priority > resolvedPriority) {
124
+ resolvedPriority = priority;
125
+ resolved = candidate;
126
+ }
127
+ }
128
+
129
+ return resolved;
130
+ }
131
+
132
+ /** キーは ChangeCategory 語彙順に正規化する(equals をキー順に依存させないため) */
133
+ toRecord(): Readonly<Record<string, readonly string[]>> {
134
+ const record: Record<string, readonly string[]> = {};
135
+ for (const category of CHANGE_CATEGORY_VALUES) {
136
+ const globs = this.patterns.get(category);
137
+ if (globs !== undefined) {
138
+ record[category] = globs;
139
+ }
140
+ }
141
+ return Object.freeze(record);
142
+ }
143
+
144
+ equals(other: CategoryOverrideRules): boolean {
145
+ return JSON.stringify(this.toRecord()) === JSON.stringify(other.toRecord());
146
+ }
147
+ }
@@ -24,6 +24,31 @@ const VALID_CATEGORIES: readonly ChangeCategoryValue[] = [
24
24
  'api',
25
25
  ] as const;
26
26
 
27
+ /**
28
+ * ChangeCategory の語彙。config の enum 検証や categoryOverrides の
29
+ * キー検証はこの定義を唯一の権威として参照する。
30
+ * @work-item-id WI-372
31
+ */
32
+ export const CHANGE_CATEGORY_VALUES: readonly ChangeCategoryValue[] = VALID_CATEGORIES;
33
+
34
+ /** 与えられた文字列が ChangeCategory の語彙に含まれるか(正規化はしない) */
35
+ export function isChangeCategoryValue(raw: string): raw is ChangeCategoryValue {
36
+ return VALID_CATEGORIES.includes(raw as ChangeCategoryValue);
37
+ }
38
+
39
+ // リスク順優先度(api > domain > feature > bugfix > test > config > docs)。
40
+ // 複数の分類条件に一致した場合に、より高リスク側へ倒すための順序定義。
41
+ // @work-item-id WI-372
42
+ const RISK_PRIORITY: Record<ChangeCategoryValue, number> = {
43
+ api: 6,
44
+ domain: 5,
45
+ feature: 4,
46
+ bugfix: 3,
47
+ test: 2,
48
+ config: 1,
49
+ docs: 0,
50
+ };
51
+
27
52
  export class UnknownChangeCategoryError extends Error {
28
53
  constructor(raw: string) {
29
54
  super(`Unknown change category: "${raw}". Valid values: ${VALID_CATEGORIES.join(', ')}`);
@@ -50,6 +75,14 @@ export class ChangeCategory {
50
75
  return this.value === 'domain' || this.value === 'feature' || this.value === 'api';
51
76
  }
52
77
 
78
+ /**
79
+ * リスク優先度。値が大きいほど高リスク(api=6 ... docs=0)。
80
+ * @work-item-id WI-372
81
+ */
82
+ riskPriority(): number {
83
+ return RISK_PRIORITY[this.value];
84
+ }
85
+
53
86
  toString(): string {
54
87
  return this.value;
55
88
  }