phasegate 0.315.0 → 0.340.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.ja.md +22 -9
- package/README.md +28 -17
- package/docs/ADR/038-config-state-operation-permission-policy.md +5 -2
- package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
- package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
- package/docs/ADR/041-non-excludable-agent-trust-roots.md +68 -0
- package/docs/guide/antigravity-integration.md +26 -0
- package/docs/guide/cli-reference.md +93 -11
- package/docs/guide/codex-integration.md +44 -107
- package/docs/guide/configuration.md +43 -2
- package/docs/guide/grok-integration.md +26 -0
- package/docs/guide/hooks-integration.md +27 -4
- package/docs/guide/installation.md +4 -2
- package/docs/guide/quick-vs-full-mode.md +36 -1
- package/docs/guide/setup-artifacts.md +4 -4
- package/docs/guide/skills-overview.md +1 -1
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +6 -2
- package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +17 -9
- package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
- package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +196 -54
- package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +39 -17
- package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +1 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +129 -73
- package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
- package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
- package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
- package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
- package/scripts/harness/ci-governance/composition-root.ts +55 -3
- package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
- package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
- package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
- package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
- package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
- package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -2
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
- package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
- package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
- package/scripts/harness/installation/application/named-hook-json.ts +17 -0
- package/scripts/harness/installation/application/operator-notice.ts +42 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
- package/scripts/harness/installation/application/usecases/run-install.ts +152 -52
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +86 -10
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
- package/scripts/harness/installation/composition-root.ts +10 -0
- package/scripts/harness/installation/domain/agent-target.ts +39 -0
- package/scripts/harness/installation/domain/check-id.ts +5 -0
- package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
- package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
- package/scripts/harness/installation/domain/repair-table.ts +5 -0
- package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
- package/scripts/harness/installation/presentation/cli/install-handler.ts +23 -4
- package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
- package/scripts/harness/main.ts +202 -51
- package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
- package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +4 -1
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
- package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +59 -19
- package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
- package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
- package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
- package/scripts/harness/quick-mode/index.ts +7 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
- package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
- package/skills/product-architect/SKILL.md +19 -0
- package/skills/story-mapper/SKILL.md +11 -0
- package/skills/story-writer/SKILL.md +11 -0
- package/skills/unit-designer/SKILL.md +11 -0
- package/templates/.agents/hooks.json +16 -0
- package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
- package/templates/.claude/settings.json +5 -3
- package/templates/.codex/hooks.json +2 -2
- package/templates/product_overview.template.md +85 -0
- package/templates/product_overview_plan.template.md +55 -0
- package/templates/story_mapping_plan.template.md +61 -0
- package/templates/story_writer_plan.template.md +61 -0
- package/templates/unit_design_plan.template.md +63 -0
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit phase-dependency-model
|
|
4
|
+
* @work-item-id WI-369
|
|
4
5
|
*/
|
|
5
6
|
|
|
7
|
+
import type { PathRoots } from '../values/artifact.js';
|
|
6
8
|
import type { PhaseNode } from '../values/phase-node.js';
|
|
7
9
|
import type { PlanEvidence } from '../values/plan-evidence.js';
|
|
8
10
|
import type { PlanningMode } from '../values/planning-mode.js';
|
|
9
11
|
|
|
10
12
|
export interface PlanDocumentReaderPort {
|
|
13
|
+
/**
|
|
14
|
+
* plan 文書の存在と QA evidence を読む。
|
|
15
|
+
*
|
|
16
|
+
* `pathRoots` は `phasegate.config.json` の `paths` を解決したもの。
|
|
17
|
+
* 省略時は既定ルート(`docs/inception` / `docs/product/construction`)を使う。
|
|
18
|
+
* WI-369: これを渡さないと `paths.inceptionDocs` を移設した PJ で
|
|
19
|
+
* plan 文書だけが既定パスから探され、成果物検査と plan evidence の
|
|
20
|
+
* 探索先がズレる(成果物は在るのに「plan文書が不足」で落ちる)。
|
|
21
|
+
*/
|
|
11
22
|
readEvidence(
|
|
12
23
|
node: PhaseNode,
|
|
13
24
|
scope: { unitId?: string; storyId?: string },
|
|
14
25
|
expectedMode: PlanningMode,
|
|
26
|
+
pathRoots?: PathRoots,
|
|
15
27
|
): Promise<PlanEvidence>;
|
|
16
28
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit phase-dependency-model
|
|
4
|
+
* @work-item-id WI-369
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { readFile } from "node:fs/promises";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
import type { PlanDocumentReaderPort } from "../../domain/ports/plan-document-reader-port.js";
|
|
10
|
+
import { DEFAULT_PATH_ROOTS, type PathRoots } from "../../domain/values/artifact.js";
|
|
9
11
|
import type { PhaseNode } from "../../domain/values/phase-node.js";
|
|
10
12
|
import { PlanEvidence } from "../../domain/values/plan-evidence.js";
|
|
11
13
|
import type { PlanningMode } from "../../domain/values/planning-mode.js";
|
|
@@ -41,6 +43,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
|
|
|
41
43
|
node: PhaseNode,
|
|
42
44
|
scope: { unitId?: string; storyId?: string },
|
|
43
45
|
expectedMode: PlanningMode,
|
|
46
|
+
pathRoots: PathRoots = DEFAULT_PATH_ROOTS,
|
|
44
47
|
): Promise<PlanEvidence> {
|
|
45
48
|
const planArtifacts = node.planArtifacts();
|
|
46
49
|
|
|
@@ -55,7 +58,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
|
|
|
55
58
|
const artifact = planArtifacts[0];
|
|
56
59
|
let resolvedPath: string;
|
|
57
60
|
try {
|
|
58
|
-
resolvedPath = artifact.resolve(scope);
|
|
61
|
+
resolvedPath = artifact.resolve(scope, pathRoots);
|
|
59
62
|
} catch {
|
|
60
63
|
return PlanEvidence.create({
|
|
61
64
|
exists: false,
|
package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts
CHANGED
|
@@ -11,6 +11,6 @@ export interface ChangeCategoryClassificationContract {
|
|
|
11
11
|
readonly dominantCategory: string | null;
|
|
12
12
|
readonly perFile: readonly ChangeCategoryPerFile[];
|
|
13
13
|
readonly fullModeRequired: boolean;
|
|
14
|
-
readonly rejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
14
|
+
readonly rejectionRule?: 'CATEGORY_NOT_ALLOWED' | 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
15
15
|
readonly rejectionReason?: string;
|
|
16
16
|
}
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
export interface QuickModeEligibilityContract {
|
|
9
9
|
readonly eligible: boolean;
|
|
10
10
|
readonly reason: string;
|
|
11
|
-
readonly rejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
11
|
+
readonly rejectionRule?: 'CATEGORY_NOT_ALLOWED' | 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
12
12
|
readonly rejectedFiles?: readonly { filePath: string; changeKind: string }[];
|
|
13
13
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @layer application
|
|
3
3
|
* @unit quick-mode
|
|
4
4
|
* @story H10-05
|
|
5
|
+
* @work-item-id WI-384
|
|
5
6
|
*
|
|
6
7
|
* paths から変更カテゴリを分類し fullModeRequired 判定と理由を返す UseCase
|
|
7
8
|
*/
|
|
@@ -20,6 +21,7 @@ export interface ClassifyChangeCategoryUseCaseInput {
|
|
|
20
21
|
readonly paths: readonly string[];
|
|
21
22
|
readonly targetChanges?: readonly {
|
|
22
23
|
readonly filePath: string;
|
|
24
|
+
readonly changeKind?: ChangeKind;
|
|
23
25
|
readonly beforeContent?: string | null;
|
|
24
26
|
readonly afterContent?: string | null;
|
|
25
27
|
}[];
|
|
@@ -94,7 +96,7 @@ export class ClassifyChangeCategoryUseCase {
|
|
|
94
96
|
// 変更前の内容が無く変更後の内容がある場合は新規作成 (CREATE) とみなす。
|
|
95
97
|
// 以前は無条件で MODIFY 固定だったため、新規 domain/ ファイルが
|
|
96
98
|
// NEW_DOMAIN 判定を回避して quick mode をすり抜けていた。
|
|
97
|
-
changeKind: beforeContent === null && afterContent !== null ? "CREATE" : "MODIFY",
|
|
99
|
+
changeKind: targetChange?.changeKind ?? (beforeContent === null && afterContent !== null ? "CREATE" : "MODIFY"),
|
|
98
100
|
beforeContent,
|
|
99
101
|
afterContent,
|
|
100
102
|
});
|
|
@@ -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
|
+
}
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* @work-item-id WI-204
|
|
5
5
|
* @work-item-id WI-349
|
|
6
6
|
* @work-item-id WI-352
|
|
7
|
+
* @work-item-id WI-372
|
|
8
|
+
* @work-item-id WI-390
|
|
7
9
|
*
|
|
8
10
|
* ChangedFile[]をChangeClassificationに変換し、3拒否ルールを評価してQuickModeEligibilityを返すドメインサービス
|
|
9
11
|
*/
|
|
10
12
|
|
|
13
|
+
import { CategoryOverrideRules } from "../value-objects/category-override-rules.js";
|
|
11
14
|
import { ChangeCategory } from "../value-objects/change-category.js";
|
|
12
15
|
import { ChangeClassification } from "../value-objects/change-classification.js";
|
|
13
16
|
import type { ChangedFile } from "../value-objects/changed-file.js";
|
|
@@ -15,17 +18,6 @@ import type { QuickModeConfig } from "../value-objects/quick-mode-config.js";
|
|
|
15
18
|
import { QuickModeEligibility } from "../value-objects/quick-mode-eligibility.js";
|
|
16
19
|
import { isCommentOnlyDiff } from "./comment-only-diff-detector.js";
|
|
17
20
|
|
|
18
|
-
// リスク順優先度(api > domain > feature > bugfix > test > config > docs)
|
|
19
|
-
const RISK_PRIORITY: Record<string, number> = {
|
|
20
|
-
api: 6,
|
|
21
|
-
domain: 5,
|
|
22
|
-
feature: 4,
|
|
23
|
-
bugfix: 3,
|
|
24
|
-
test: 2,
|
|
25
|
-
config: 1,
|
|
26
|
-
docs: 0,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
21
|
// config: リポジトリ直下の bootstrap 設定ファイル(列挙型 allowlist)。
|
|
30
22
|
// これらは unit を持たず、CREATE だと feature に落ちて allowedCategories に
|
|
31
23
|
// 入れる手段がないため greenfield な初期セットアップが恒久的にブロックされていた。
|
|
@@ -57,7 +49,7 @@ function isRootBootstrapConfigFile(filePath: string): boolean {
|
|
|
57
49
|
return filePath.startsWith(HUSKY_DIRECTORY_PREFIX) && filePath.slice(HUSKY_DIRECTORY_PREFIX.length).length > 0;
|
|
58
50
|
}
|
|
59
51
|
|
|
60
|
-
function
|
|
52
|
+
function categorizeFileByBuiltInRules(file: ChangedFile): ChangeCategory {
|
|
61
53
|
const { filePath, changeKind } = file;
|
|
62
54
|
|
|
63
55
|
// Config files must stay config even when Edit payload snippets look like
|
|
@@ -101,6 +93,15 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
|
|
|
101
93
|
return ChangeCategory.fromString("test");
|
|
102
94
|
}
|
|
103
95
|
|
|
96
|
+
// Markdown は配置場所にかかわらず文書として扱う。config / API / test の
|
|
97
|
+
// 明示ルールは優先しつつ、domain ディレクトリや CREATE フォールバックより
|
|
98
|
+
// 前に評価して README.md / *.mdx の新規作成を feature/domain に誤分類しない。
|
|
99
|
+
// 信頼境界となる root agent instructions は protected-file 側で別途保護する。
|
|
100
|
+
// @work-item-id WI-390
|
|
101
|
+
if (filePath.endsWith(".md") || filePath.endsWith(".mdx")) {
|
|
102
|
+
return ChangeCategory.fromString("docs");
|
|
103
|
+
}
|
|
104
|
+
|
|
104
105
|
// domain: domain/ 配下
|
|
105
106
|
if (filePath.includes("/domain/") || filePath.startsWith("domain/")) {
|
|
106
107
|
return ChangeCategory.fromString("domain");
|
|
@@ -130,6 +131,43 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
|
|
|
130
131
|
return ChangeCategory.fromString("bugfix");
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
// 組み込み判定が構造的に高リスク(domain / api)のカテゴリは override で降格できない。
|
|
135
|
+
// @work-item-id WI-372
|
|
136
|
+
const NON_DOWNGRADABLE_BUILT_IN_CATEGORIES: ReadonlySet<string> = new Set(["domain", "api"]);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* WI-372: `quickMode.categoryOverrides` を反映したカテゴリ判定。
|
|
140
|
+
*
|
|
141
|
+
* override は「このパスはこの種類の変更である」という利用者の明示宣言なので、
|
|
142
|
+
* 組み込みルールより **先** に評価する(DD-1)。組み込みの後段に置くと
|
|
143
|
+
* `notes/x.config.json` のような偶発的な組み込みマッチに常に負け、
|
|
144
|
+
* 「設定したのに効かない」不可解な挙動になるため。
|
|
145
|
+
*
|
|
146
|
+
* ただし組み込み判定が `domain` / `api` のファイルは override で降格できない(DD-2)。
|
|
147
|
+
* `judge()` の NEW_DOMAIN は CREATE のみ、API_CONTRACT は port/adapter のみを見るため、
|
|
148
|
+
* ガードが無いと domain ファイルの MODIFY が `docs` として素通りしてしまう。
|
|
149
|
+
* 降格は禁じるが昇格(domain → api 等)は許す。
|
|
150
|
+
*
|
|
151
|
+
* override 未設定時は組み込み分類と完全に一致する(後方互換)。
|
|
152
|
+
*/
|
|
153
|
+
function categorizeFile(file: ChangedFile, overrides: CategoryOverrideRules): ChangeCategory {
|
|
154
|
+
const builtIn = categorizeFileByBuiltInRules(file);
|
|
155
|
+
if (overrides.isEmpty()) {
|
|
156
|
+
return builtIn;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const override = overrides.resolve(file.filePath);
|
|
160
|
+
if (override === null) {
|
|
161
|
+
return builtIn;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (NON_DOWNGRADABLE_BUILT_IN_CATEGORIES.has(builtIn.toString())) {
|
|
165
|
+
return override.riskPriority() > builtIn.riskPriority() ? override : builtIn;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return override;
|
|
169
|
+
}
|
|
170
|
+
|
|
133
171
|
/**
|
|
134
172
|
* WI-349: 遮断理由に判定根拠(分類カテゴリと変更種別)を含める。
|
|
135
173
|
*
|
|
@@ -142,17 +180,18 @@ function describeChangedFile(file: ChangedFile, category: string): string {
|
|
|
142
180
|
}
|
|
143
181
|
|
|
144
182
|
export class QuickModeJudgmentEngine {
|
|
145
|
-
classify(changedFiles: readonly ChangedFile[],
|
|
183
|
+
classify(changedFiles: readonly ChangedFile[], config?: QuickModeConfig): ChangeClassification {
|
|
146
184
|
if (changedFiles.length === 0) {
|
|
147
185
|
return new ChangeClassification(null, new Map(), 0);
|
|
148
186
|
}
|
|
149
187
|
|
|
188
|
+
const overrides = config?.categoryOverrides ?? CategoryOverrideRules.empty();
|
|
150
189
|
const categorizedMap = new Map<string, ChangedFile[]>();
|
|
151
190
|
let dominantCategory: ChangeCategory | null = null;
|
|
152
191
|
let dominantPriority = -1;
|
|
153
192
|
|
|
154
193
|
for (const file of changedFiles) {
|
|
155
|
-
const category = categorizeFile(file);
|
|
194
|
+
const category = categorizeFile(file, overrides);
|
|
156
195
|
const key = category.toString();
|
|
157
196
|
|
|
158
197
|
if (!categorizedMap.has(key)) {
|
|
@@ -160,7 +199,7 @@ export class QuickModeJudgmentEngine {
|
|
|
160
199
|
}
|
|
161
200
|
categorizedMap.get(key)!.push(file);
|
|
162
201
|
|
|
163
|
-
const priority =
|
|
202
|
+
const priority = category.riskPriority();
|
|
164
203
|
if (priority > dominantPriority) {
|
|
165
204
|
dominantPriority = priority;
|
|
166
205
|
dominantCategory = category;
|
|
@@ -173,7 +212,7 @@ export class QuickModeJudgmentEngine {
|
|
|
173
212
|
judge(changedFiles: readonly ChangedFile[], config: QuickModeConfig): QuickModeEligibility {
|
|
174
213
|
const classification = this.classify(changedFiles, config);
|
|
175
214
|
|
|
176
|
-
// 1.
|
|
215
|
+
// 1. カテゴリ評価: 不許可カテゴリを含む単一カテゴリ変更と、複数カテゴリ変更を区別する。
|
|
177
216
|
if (config.isFullModeRequiredFor("mixedCategories")) {
|
|
178
217
|
const notAllowedFiles: ChangedFile[] = [];
|
|
179
218
|
const notAllowedDescriptions: string[] = [];
|
|
@@ -187,8 +226,9 @@ export class QuickModeJudgmentEngine {
|
|
|
187
226
|
});
|
|
188
227
|
|
|
189
228
|
if (notAllowedFiles.length > 0) {
|
|
229
|
+
const rejectionRule = classification.categorizedFiles.size >= 2 ? "MIXED_CHANGES" : "CATEGORY_NOT_ALLOWED";
|
|
190
230
|
return QuickModeEligibility.rejected(
|
|
191
|
-
|
|
231
|
+
rejectionRule,
|
|
192
232
|
notAllowedFiles,
|
|
193
233
|
`allowedCategories外のファイルが含まれています: ${notAllowedDescriptions.join(", ")}`,
|
|
194
234
|
);
|
|
@@ -206,7 +246,7 @@ export class QuickModeJudgmentEngine {
|
|
|
206
246
|
"NEW_DOMAIN",
|
|
207
247
|
newDomainFiles,
|
|
208
248
|
`domain/配下に新規ファイルが追加されています: ${newDomainFiles
|
|
209
|
-
.map((f) => describeChangedFile(f, categorizeFile(f).toString()))
|
|
249
|
+
.map((f) => describeChangedFile(f, categorizeFile(f, config.categoryOverrides).toString()))
|
|
210
250
|
.join(", ")}`,
|
|
211
251
|
);
|
|
212
252
|
}
|
|
@@ -223,7 +263,7 @@ export class QuickModeJudgmentEngine {
|
|
|
223
263
|
"API_CONTRACT",
|
|
224
264
|
apiContractFiles,
|
|
225
265
|
`Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles
|
|
226
|
-
.map((f) => describeChangedFile(f, categorizeFile(f).toString()))
|
|
266
|
+
.map((f) => describeChangedFile(f, categorizeFile(f, config.categoryOverrides).toString()))
|
|
227
267
|
.join(", ")}`,
|
|
228
268
|
);
|
|
229
269
|
}
|
|
@@ -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
|
}
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* Quick Mode設定を表す値オブジェクト
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 {
|
|
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';
|